@nmakarov/cli-toolkit 0.14.2 → 0.18.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.
- package/dist/args.cjs +49 -9
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +49 -9
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +5006 -0
- package/dist/cli-runner.cjs.map +1 -0
- package/dist/cli-runner.js +4989 -0
- package/dist/cli-runner.js.map +1 -0
- package/dist/db.cjs +9 -2
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +9 -2
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +17 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +110 -37
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +110 -36
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +44 -34
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +44 -34
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +1728 -0
- package/dist/http-client2.cjs.map +1 -0
- package/dist/http-client2.js +1690 -0
- package/dist/http-client2.js.map +1 -0
- package/dist/index.cjs +1456 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1436 -110
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +199 -82
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +199 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +28 -42
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +28 -42
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +205 -359
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +203 -359
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +114 -15
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +114 -15
- package/dist/params.js.map +1 -1
- package/dist/tasks.cjs +2295 -0
- package/dist/tasks.cjs.map +1 -0
- package/dist/tasks.js +2240 -0
- package/dist/tasks.js.map +1 -0
- package/dist/utils.cjs +15 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +12 -1
- package/dist/utils.js.map +1 -1
- package/package.json +18 -3
|
@@ -0,0 +1,4989 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/screen/components.ts
|
|
19
|
+
import { createElement as h } from "react";
|
|
20
|
+
import { Box, Text } from "ink";
|
|
21
|
+
function getScreenWidth(maxWidth = null) {
|
|
22
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
23
|
+
const availableWidth = Math.max(20, terminalWidth - 4);
|
|
24
|
+
return maxWidth ? Math.min(availableWidth, maxWidth) : availableWidth;
|
|
25
|
+
}
|
|
26
|
+
function ScreenContainer({ children }) {
|
|
27
|
+
const width = getScreenWidth();
|
|
28
|
+
return h(Box, {
|
|
29
|
+
flexDirection: "column",
|
|
30
|
+
marginTop: 1,
|
|
31
|
+
borderStyle: "single",
|
|
32
|
+
borderColor: "cyan",
|
|
33
|
+
paddingX: 1,
|
|
34
|
+
width
|
|
35
|
+
// Use the calculated width directly
|
|
36
|
+
}, children);
|
|
37
|
+
}
|
|
38
|
+
function ScreenRow({ children }) {
|
|
39
|
+
return h(Box, { flexDirection: "column" }, children);
|
|
40
|
+
}
|
|
41
|
+
function ScreenTitle({ text }) {
|
|
42
|
+
return h(
|
|
43
|
+
ScreenRow,
|
|
44
|
+
{},
|
|
45
|
+
h(Text, { bold: true, color: "cyan" }, text)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function ScreenDivider({ width }) {
|
|
49
|
+
const dividerWidth = width || getScreenWidth() - 4;
|
|
50
|
+
return h(Text, { color: "cyan", dimColor: true }, "\u2500".repeat(dividerWidth));
|
|
51
|
+
}
|
|
52
|
+
function ScreenBody({ children, alignItems = "flex-start" }) {
|
|
53
|
+
return h(Box, { flexDirection: "column", alignItems }, children);
|
|
54
|
+
}
|
|
55
|
+
function ScreenFooter({ lines, textStyle }) {
|
|
56
|
+
const defaultTextStyle = {
|
|
57
|
+
dimColor: true,
|
|
58
|
+
color: "white"
|
|
59
|
+
};
|
|
60
|
+
const finalTextStyle = { ...defaultTextStyle, ...textStyle };
|
|
61
|
+
const flattenAndWrap = (items, keyPrefix = "") => {
|
|
62
|
+
const result = [];
|
|
63
|
+
let keyIndex = 0;
|
|
64
|
+
items.forEach((item, index) => {
|
|
65
|
+
if (Array.isArray(item)) {
|
|
66
|
+
const nested = flattenAndWrap(item, `${keyPrefix}-${index}`);
|
|
67
|
+
result.push(...nested);
|
|
68
|
+
} else if (typeof item === "string") {
|
|
69
|
+
result.push(
|
|
70
|
+
h(Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, item)
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
const element = item;
|
|
74
|
+
if (element.key === null || element.key === void 0) {
|
|
75
|
+
result.push(
|
|
76
|
+
h(Text, { key: `${keyPrefix}-${keyIndex++}`, ...finalTextStyle }, element)
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
result.push(element);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
const wrappedItems = flattenAndWrap(lines);
|
|
86
|
+
return h(
|
|
87
|
+
Box,
|
|
88
|
+
{ flexDirection: "column" },
|
|
89
|
+
h(Box, { flexDirection: "row" }, ...wrappedItems)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
var init_components = __esm({
|
|
93
|
+
"src/screen/components.ts"() {
|
|
94
|
+
"use strict";
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// src/screen/list-components.ts
|
|
99
|
+
import React2, { useState, useEffect, useRef, createElement } from "react";
|
|
100
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
101
|
+
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
102
|
+
const [, forceUpdate] = useState({});
|
|
103
|
+
const termWidth = (process.stdout.columns || 80) - 8;
|
|
104
|
+
const maxItemLength = Math.max(...items.map((w) => w.length));
|
|
105
|
+
const columnWidth = maxItemLength + 3;
|
|
106
|
+
const columns = Math.max(1, Math.floor(termWidth / columnWidth));
|
|
107
|
+
const itemsPerColumn = Math.ceil(items.length / columns);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
ctx.setAction("moveUp", () => {
|
|
110
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
|
|
111
|
+
forceUpdate({});
|
|
112
|
+
});
|
|
113
|
+
ctx.setAction("moveDown", () => {
|
|
114
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
|
|
115
|
+
forceUpdate({});
|
|
116
|
+
});
|
|
117
|
+
ctx.setAction("moveLeft", () => {
|
|
118
|
+
if (selectedIndexRef.current === 0) {
|
|
119
|
+
ctx.goBack();
|
|
120
|
+
} else {
|
|
121
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
|
|
122
|
+
forceUpdate({});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
ctx.setAction("moveRight", () => {
|
|
126
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
|
|
127
|
+
forceUpdate({});
|
|
128
|
+
});
|
|
129
|
+
ctx.setKeyBinding([
|
|
130
|
+
{ key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
|
|
131
|
+
{ key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
|
|
132
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
133
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
134
|
+
]);
|
|
135
|
+
ctx.addFooter(`Total: ${items.length} items`);
|
|
136
|
+
}, []);
|
|
137
|
+
const selectedIndex = selectedIndexRef.current;
|
|
138
|
+
const rows = [];
|
|
139
|
+
for (let row = 0; row < itemsPerColumn; row++) {
|
|
140
|
+
const cols = [];
|
|
141
|
+
for (let col = 0; col < columns; col++) {
|
|
142
|
+
const index = col * itemsPerColumn + row;
|
|
143
|
+
if (index < items.length) {
|
|
144
|
+
const isSelected = index === selectedIndex;
|
|
145
|
+
cols.push(
|
|
146
|
+
h2(
|
|
147
|
+
Box2,
|
|
148
|
+
{ key: index, width: columnWidth },
|
|
149
|
+
h2(Text2, {
|
|
150
|
+
color: isSelected ? "black" : "white",
|
|
151
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
152
|
+
bold: isSelected
|
|
153
|
+
}, items[index].padEnd(maxItemLength))
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
rows.push(
|
|
159
|
+
h2(ScreenRow, { key: row, children: h2(Box2, { flexDirection: "row" }, ...cols) })
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return h2(Box2, { flexDirection: "column" }, ...rows);
|
|
163
|
+
}
|
|
164
|
+
function MultiColumnListWithPreviewComponent({
|
|
165
|
+
items,
|
|
166
|
+
getPreviewContent,
|
|
167
|
+
ctx,
|
|
168
|
+
selectedIndexRef
|
|
169
|
+
}) {
|
|
170
|
+
const [, forceUpdate] = useState({});
|
|
171
|
+
const termWidth = (process.stdout.columns || 80) - 8;
|
|
172
|
+
const maxItemLength = Math.max(...items.map((w) => w.length));
|
|
173
|
+
const columnWidth = maxItemLength + 3;
|
|
174
|
+
const columns = Math.max(1, Math.floor(termWidth / columnWidth));
|
|
175
|
+
const itemsPerColumn = Math.ceil(items.length / columns);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
ctx.setAction("moveUp", () => {
|
|
178
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - 1);
|
|
179
|
+
forceUpdate({});
|
|
180
|
+
});
|
|
181
|
+
ctx.setAction("moveDown", () => {
|
|
182
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + 1);
|
|
183
|
+
forceUpdate({});
|
|
184
|
+
});
|
|
185
|
+
ctx.setAction("moveLeft", () => {
|
|
186
|
+
if (selectedIndexRef.current === 0) {
|
|
187
|
+
ctx.goBack();
|
|
188
|
+
} else {
|
|
189
|
+
selectedIndexRef.current = Math.max(0, selectedIndexRef.current - itemsPerColumn);
|
|
190
|
+
forceUpdate({});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
ctx.setAction("moveRight", () => {
|
|
194
|
+
selectedIndexRef.current = Math.min(items.length - 1, selectedIndexRef.current + itemsPerColumn);
|
|
195
|
+
forceUpdate({});
|
|
196
|
+
});
|
|
197
|
+
ctx.setKeyBinding([
|
|
198
|
+
{ key: "leftArrow", caption: "navigate", action: "moveLeft", order: 0 },
|
|
199
|
+
{ key: "rightArrow", caption: "navigate", action: "moveRight", order: 0 },
|
|
200
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
201
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
202
|
+
]);
|
|
203
|
+
ctx.addFooter(`Total: ${items.length} items`);
|
|
204
|
+
}, []);
|
|
205
|
+
const selectedIndex = selectedIndexRef.current;
|
|
206
|
+
const selectedItem = items[selectedIndex];
|
|
207
|
+
const rows = [];
|
|
208
|
+
for (let row = 0; row < itemsPerColumn; row++) {
|
|
209
|
+
const cols = [];
|
|
210
|
+
for (let col = 0; col < columns; col++) {
|
|
211
|
+
const index = col * itemsPerColumn + row;
|
|
212
|
+
if (index < items.length) {
|
|
213
|
+
const isSelected = index === selectedIndex;
|
|
214
|
+
cols.push(
|
|
215
|
+
h2(
|
|
216
|
+
Box2,
|
|
217
|
+
{ key: index, width: columnWidth },
|
|
218
|
+
h2(Text2, {
|
|
219
|
+
color: isSelected ? "black" : "white",
|
|
220
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
221
|
+
bold: isSelected
|
|
222
|
+
}, items[index].padEnd(maxItemLength))
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
rows.push(
|
|
228
|
+
h2(ScreenRow, { key: row, children: h2(Box2, { flexDirection: "row" }, ...cols) })
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const previewContent = getPreviewContent ? getPreviewContent(selectedItem) : selectedItem;
|
|
232
|
+
const previewRows = [];
|
|
233
|
+
if (typeof previewContent === "string") {
|
|
234
|
+
previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
|
|
235
|
+
} else if (typeof previewContent === "object" && !React2.isValidElement(previewContent) && previewContent !== null) {
|
|
236
|
+
Object.entries(previewContent).forEach(([key, value], idx) => {
|
|
237
|
+
previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
|
|
238
|
+
});
|
|
239
|
+
} else if (React2.isValidElement(previewContent)) {
|
|
240
|
+
previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
|
|
241
|
+
}
|
|
242
|
+
return h2(
|
|
243
|
+
Box2,
|
|
244
|
+
{ flexDirection: "column" },
|
|
245
|
+
...rows,
|
|
246
|
+
h2(ScreenRow, { key: "spacer-1", children: h2(Text2, {}, " ") }),
|
|
247
|
+
h2(ScreenDivider, { key: "divider" }),
|
|
248
|
+
h2(ScreenRow, { key: "spacer-2", children: h2(Text2, {}, " ") }),
|
|
249
|
+
...previewRows
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
|
|
253
|
+
const [, forceUpdate] = useState({});
|
|
254
|
+
const [sortOrder, setSortOrder] = useState("none");
|
|
255
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
256
|
+
const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
|
|
257
|
+
const defaultGetTitle = (item) => {
|
|
258
|
+
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
259
|
+
};
|
|
260
|
+
const titleGetter = getTitle || defaultGetTitle;
|
|
261
|
+
const displayItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
|
|
262
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
263
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
264
|
+
if (sortOrder === "asc") {
|
|
265
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
266
|
+
} else {
|
|
267
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
268
|
+
}
|
|
269
|
+
}) : items;
|
|
270
|
+
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
271
|
+
const canScroll = displayItems.length > effectiveMaxHeight;
|
|
272
|
+
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
273
|
+
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
274
|
+
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
275
|
+
const canScrollUp = clampedScrollOffset > 0;
|
|
276
|
+
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
277
|
+
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
ctx.setAction("moveUp", () => {
|
|
280
|
+
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
281
|
+
selectedIndexRef.current = newIndex;
|
|
282
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
283
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
284
|
+
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
285
|
+
if (newIndex < currentClampedScrollOffset) {
|
|
286
|
+
setScrollOffset(newIndex);
|
|
287
|
+
}
|
|
288
|
+
forceUpdate({});
|
|
289
|
+
});
|
|
290
|
+
ctx.setAction("moveDown", () => {
|
|
291
|
+
const currentItems = sortable && sortOrder !== "none" ? [...items].sort((a, b) => {
|
|
292
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
293
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
294
|
+
if (sortOrder === "asc") {
|
|
295
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
296
|
+
} else {
|
|
297
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
298
|
+
}
|
|
299
|
+
}) : items;
|
|
300
|
+
const maxIndex = currentItems.length - 1;
|
|
301
|
+
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
302
|
+
selectedIndexRef.current = newIndex;
|
|
303
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
304
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
305
|
+
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
306
|
+
if (newIndex >= currentClampedScrollOffset + currentMaxHeight) {
|
|
307
|
+
setScrollOffset(newIndex - currentMaxHeight + 1);
|
|
308
|
+
}
|
|
309
|
+
forceUpdate({});
|
|
310
|
+
});
|
|
311
|
+
ctx.setAction("scrollUp", () => {
|
|
312
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
313
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
314
|
+
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
315
|
+
setScrollOffset(newScrollOffset);
|
|
316
|
+
forceUpdate({});
|
|
317
|
+
});
|
|
318
|
+
ctx.setAction("scrollDown", () => {
|
|
319
|
+
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
320
|
+
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
321
|
+
const newScrollOffset = Math.min(currentMaxScrollOffset, currentScrollOffset + 1);
|
|
322
|
+
setScrollOffset(newScrollOffset);
|
|
323
|
+
forceUpdate({});
|
|
324
|
+
});
|
|
325
|
+
if (sortable) {
|
|
326
|
+
ctx.setAction("toggleSort", () => {
|
|
327
|
+
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
328
|
+
const currentSelectedItem = displayItems[selectedIndexRef.current];
|
|
329
|
+
setSortOrder(nextSort);
|
|
330
|
+
const newSortedItems = nextSort !== "none" ? [...items].sort((a, b) => {
|
|
331
|
+
const titleA = titleGetter(a).toLowerCase();
|
|
332
|
+
const titleB = titleGetter(b).toLowerCase();
|
|
333
|
+
if (nextSort === "asc") {
|
|
334
|
+
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
335
|
+
} else {
|
|
336
|
+
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
337
|
+
}
|
|
338
|
+
}) : items;
|
|
339
|
+
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
340
|
+
if (newIndex !== -1) {
|
|
341
|
+
selectedIndexRef.current = newIndex;
|
|
342
|
+
setScrollOffset(newIndex);
|
|
343
|
+
} else {
|
|
344
|
+
selectedIndexRef.current = 0;
|
|
345
|
+
setScrollOffset(0);
|
|
346
|
+
}
|
|
347
|
+
forceUpdate({});
|
|
348
|
+
});
|
|
349
|
+
const defaultHighlightStyle = {
|
|
350
|
+
color: "black",
|
|
351
|
+
backgroundColor: "green",
|
|
352
|
+
bold: true
|
|
353
|
+
};
|
|
354
|
+
const highlightStyle = { ...defaultHighlightStyle, ...sortHighlightStyle };
|
|
355
|
+
const sortCaption = () => {
|
|
356
|
+
if (sortOrder === "none") {
|
|
357
|
+
return h2(Text2, {}, "s to toggle sort");
|
|
358
|
+
} else {
|
|
359
|
+
const sortLabel = sortOrder === "asc" ? "ASC" : "DESC";
|
|
360
|
+
return h2(
|
|
361
|
+
Text2,
|
|
362
|
+
{},
|
|
363
|
+
"s to toggle ",
|
|
364
|
+
h2(Text2, { color: "white", bold: true }, "sort"),
|
|
365
|
+
" ",
|
|
366
|
+
h2(Text2, highlightStyle, ` ${sortLabel} `)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
ctx.setKeyBinding([
|
|
371
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
372
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 },
|
|
373
|
+
{
|
|
374
|
+
key: "s",
|
|
375
|
+
caption: sortCaption,
|
|
376
|
+
action: "toggleSort",
|
|
377
|
+
order: 5
|
|
378
|
+
}
|
|
379
|
+
]);
|
|
380
|
+
ctx.update();
|
|
381
|
+
} else {
|
|
382
|
+
ctx.setKeyBinding([
|
|
383
|
+
{ key: "upArrow", caption: "navigate", action: "moveUp", order: 0 },
|
|
384
|
+
{ key: "downArrow", caption: "navigate", action: "moveDown", order: 0 }
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
}, [sortOrder, sortable]);
|
|
388
|
+
const selectedIndex = selectedIndexRef.current;
|
|
389
|
+
const defaultRenderItem = (item, isSelected, displayIndex, actualIndex) => {
|
|
390
|
+
const isFirstVisible = displayIndex === 0;
|
|
391
|
+
const isLastVisible = displayIndex === visibleItems.length - 1;
|
|
392
|
+
let arrowPrefix = "";
|
|
393
|
+
let selectionPrefix = "";
|
|
394
|
+
if (isFirstVisible && canScrollUp) {
|
|
395
|
+
arrowPrefix = "\u2191 ";
|
|
396
|
+
} else if (isLastVisible && canScrollDown) {
|
|
397
|
+
arrowPrefix = "\u2193 ";
|
|
398
|
+
} else {
|
|
399
|
+
arrowPrefix = " ";
|
|
400
|
+
}
|
|
401
|
+
if (isSelected) {
|
|
402
|
+
selectionPrefix = selectionMarker;
|
|
403
|
+
} else {
|
|
404
|
+
selectionPrefix = " ".repeat(selectionMarker.length);
|
|
405
|
+
}
|
|
406
|
+
return h2(
|
|
407
|
+
Box2,
|
|
408
|
+
{ flexDirection: "row" },
|
|
409
|
+
// Arrow (clickable if functional, not highlighted)
|
|
410
|
+
h2(Text2, {
|
|
411
|
+
key: `arrow-${actualIndex}`,
|
|
412
|
+
color: "white"
|
|
413
|
+
}, arrowPrefix),
|
|
414
|
+
// Selection marker space (always same width, not highlighted)
|
|
415
|
+
h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
|
|
416
|
+
// Item name (highlighted if selected)
|
|
417
|
+
h2(Text2, {
|
|
418
|
+
key: `name-${actualIndex}`,
|
|
419
|
+
color: isSelected ? "black" : "white",
|
|
420
|
+
backgroundColor: isSelected ? "cyan" : void 0,
|
|
421
|
+
bold: isSelected
|
|
422
|
+
}, item.name)
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
const itemRenderer = renderItem || defaultRenderItem;
|
|
426
|
+
return h2(
|
|
427
|
+
Box2,
|
|
428
|
+
{ flexDirection: "column" },
|
|
429
|
+
...visibleItems.map((item, displayIndex) => {
|
|
430
|
+
const actualIndex = clampedScrollOffset + displayIndex;
|
|
431
|
+
const isSelected = actualIndex === selectedIndex;
|
|
432
|
+
if (renderItem) {
|
|
433
|
+
const isFirstVisible = displayIndex === 0;
|
|
434
|
+
const isLastVisible = displayIndex === visibleItems.length - 1;
|
|
435
|
+
let arrowPrefix = "";
|
|
436
|
+
let selectionPrefix = "";
|
|
437
|
+
if (isFirstVisible && canScrollUp) {
|
|
438
|
+
arrowPrefix = "\u2191 ";
|
|
439
|
+
} else if (isLastVisible && canScrollDown) {
|
|
440
|
+
arrowPrefix = "\u2193 ";
|
|
441
|
+
} else {
|
|
442
|
+
arrowPrefix = " ";
|
|
443
|
+
}
|
|
444
|
+
if (isSelected) {
|
|
445
|
+
selectionPrefix = selectionMarker;
|
|
446
|
+
} else {
|
|
447
|
+
selectionPrefix = " ".repeat(selectionMarker.length);
|
|
448
|
+
}
|
|
449
|
+
return h2(ScreenRow, {
|
|
450
|
+
key: `item-${actualIndex}`,
|
|
451
|
+
children: h2(
|
|
452
|
+
Box2,
|
|
453
|
+
{ flexDirection: "row" },
|
|
454
|
+
// Arrow (clickable if functional, not highlighted)
|
|
455
|
+
h2(Text2, {
|
|
456
|
+
key: `arrow-${actualIndex}`,
|
|
457
|
+
color: "white"
|
|
458
|
+
}, arrowPrefix),
|
|
459
|
+
// Selection marker space (always same width, not highlighted)
|
|
460
|
+
h2(Text2, { key: `marker-${actualIndex}`, color: "white" }, selectionPrefix),
|
|
461
|
+
// Custom rendered content
|
|
462
|
+
renderItem(item, isSelected, displayIndex)
|
|
463
|
+
)
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
return h2(ScreenRow, {
|
|
467
|
+
key: `item-${actualIndex}`,
|
|
468
|
+
children: itemRenderer(item, isSelected, displayIndex, actualIndex)
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
var h2;
|
|
475
|
+
var init_list_components = __esm({
|
|
476
|
+
"src/screen/list-components.ts"() {
|
|
477
|
+
"use strict";
|
|
478
|
+
init_components();
|
|
479
|
+
h2 = createElement;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// src/screen/screens.ts
|
|
484
|
+
import { useState as useState2, createElement as h3 } from "react";
|
|
485
|
+
import { render, useInput, Text as Text3 } from "ink";
|
|
486
|
+
function groupKeyBindings(bindings) {
|
|
487
|
+
const groups = {};
|
|
488
|
+
const enabledBindings = bindings.filter((b) => b.enabled !== false);
|
|
489
|
+
enabledBindings.forEach((binding) => {
|
|
490
|
+
const caption = typeof binding.caption === "string" ? binding.caption : "";
|
|
491
|
+
if (!groups[caption]) {
|
|
492
|
+
groups[caption] = {
|
|
493
|
+
keys: [],
|
|
494
|
+
caption,
|
|
495
|
+
order: binding.order || 999
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
groups[caption].keys.push(binding.key);
|
|
499
|
+
});
|
|
500
|
+
return Object.values(groups);
|
|
501
|
+
}
|
|
502
|
+
function formatKeyBindings(bindings, mode = "long") {
|
|
503
|
+
const resolvedBindings = bindings.map((binding) => {
|
|
504
|
+
let resolvedCaption = binding.caption;
|
|
505
|
+
if (typeof binding.caption === "function") {
|
|
506
|
+
resolvedCaption = binding.caption();
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
...binding,
|
|
510
|
+
resolvedCaption
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
const groups = groupKeyBindings(resolvedBindings.map((b) => ({
|
|
514
|
+
...b,
|
|
515
|
+
caption: typeof b.resolvedCaption === "string" ? b.resolvedCaption : ""
|
|
516
|
+
})));
|
|
517
|
+
groups.sort((a, b) => a.order - b.order);
|
|
518
|
+
const items = [];
|
|
519
|
+
groups.forEach((group) => {
|
|
520
|
+
const bindingWithCustom = resolvedBindings.find(
|
|
521
|
+
(b) => group.keys.includes(b.key) && typeof b.resolvedCaption !== "string"
|
|
522
|
+
);
|
|
523
|
+
if (bindingWithCustom && bindingWithCustom.resolvedCaption) {
|
|
524
|
+
items.push(bindingWithCustom.resolvedCaption);
|
|
525
|
+
} else {
|
|
526
|
+
const keyStr = formatKeys(group.keys);
|
|
527
|
+
if (mode === "long") {
|
|
528
|
+
items.push(`${keyStr} to ${group.caption}`);
|
|
529
|
+
} else {
|
|
530
|
+
items.push(keyStr);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
return items;
|
|
535
|
+
}
|
|
536
|
+
function formatKeys(keys) {
|
|
537
|
+
const keyMap = {
|
|
538
|
+
"escape": "esc",
|
|
539
|
+
"leftArrow": "\u2190",
|
|
540
|
+
"rightArrow": "\u2192",
|
|
541
|
+
"upArrow": "\u2191",
|
|
542
|
+
"downArrow": "\u2193",
|
|
543
|
+
"return": "enter"
|
|
544
|
+
};
|
|
545
|
+
return keys.map((k) => keyMap[k] || k).join("/");
|
|
546
|
+
}
|
|
547
|
+
async function showScreen(config2) {
|
|
548
|
+
const {
|
|
549
|
+
title,
|
|
550
|
+
onRender,
|
|
551
|
+
parentData = {}
|
|
552
|
+
} = config2;
|
|
553
|
+
return new Promise((resolve2) => {
|
|
554
|
+
let instance;
|
|
555
|
+
const keyBindings = [];
|
|
556
|
+
const actions = {};
|
|
557
|
+
const customFooterItems = [];
|
|
558
|
+
let renderResult = null;
|
|
559
|
+
let initialized = false;
|
|
560
|
+
const Screen = () => {
|
|
561
|
+
const [updateCounter, setUpdateCounter] = useState2(0);
|
|
562
|
+
if (!initialized) {
|
|
563
|
+
const defaultBindings = [
|
|
564
|
+
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
565
|
+
{ key: "leftArrow", caption: "go back", action: "back", protected: false, order: 1 }
|
|
566
|
+
// Note: 'select' is not a default - components add it if needed
|
|
567
|
+
];
|
|
568
|
+
defaultBindings.forEach((binding) => {
|
|
569
|
+
keyBindings.push(binding);
|
|
570
|
+
});
|
|
571
|
+
actions.back = () => {
|
|
572
|
+
cleanup(null);
|
|
573
|
+
};
|
|
574
|
+
initialized = true;
|
|
575
|
+
}
|
|
576
|
+
const context = {
|
|
577
|
+
setAction: (actionName, handlerFn) => {
|
|
578
|
+
actions[actionName] = handlerFn;
|
|
579
|
+
},
|
|
580
|
+
setKeyBinding: (bindingOrBindings) => {
|
|
581
|
+
const bindingsToSet = Array.isArray(bindingOrBindings) ? bindingOrBindings : [bindingOrBindings];
|
|
582
|
+
bindingsToSet.forEach((binding) => {
|
|
583
|
+
const existingIndex = keyBindings.findIndex((b) => b.key === binding.key);
|
|
584
|
+
if (existingIndex >= 0) {
|
|
585
|
+
const existing = keyBindings[existingIndex];
|
|
586
|
+
if (existing.protected) {
|
|
587
|
+
console.warn(`Cannot override protected key: ${binding.key}`);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
keyBindings[existingIndex] = {
|
|
591
|
+
...existing,
|
|
592
|
+
...binding,
|
|
593
|
+
order: binding.order !== void 0 ? binding.order : existing.order,
|
|
594
|
+
enabled: binding.enabled !== void 0 ? binding.enabled : existing.enabled !== void 0 ? existing.enabled : true
|
|
595
|
+
};
|
|
596
|
+
} else {
|
|
597
|
+
keyBindings.push({
|
|
598
|
+
protected: false,
|
|
599
|
+
order: 999,
|
|
600
|
+
enabled: true,
|
|
601
|
+
...binding
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
},
|
|
606
|
+
updateKeyBinding: (keyName, updates) => {
|
|
607
|
+
const index = keyBindings.findIndex((b) => b.key === keyName);
|
|
608
|
+
if (index >= 0) {
|
|
609
|
+
keyBindings[index] = {
|
|
610
|
+
...keyBindings[index],
|
|
611
|
+
...updates
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
removeKeyBinding: (keyName) => {
|
|
616
|
+
const index = keyBindings.findIndex((b) => b.key === keyName);
|
|
617
|
+
if (index >= 0) {
|
|
618
|
+
if (keyBindings[index].protected) {
|
|
619
|
+
console.warn(`Cannot remove protected key: ${keyName}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
keyBindings.splice(index, 1);
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
addFooter: (item) => {
|
|
626
|
+
customFooterItems.push(item);
|
|
627
|
+
},
|
|
628
|
+
clearFooter: () => {
|
|
629
|
+
customFooterItems.length = 0;
|
|
630
|
+
},
|
|
631
|
+
setFooter: (items) => {
|
|
632
|
+
customFooterItems.length = 0;
|
|
633
|
+
const itemsArray = Array.isArray(items) ? items : [items];
|
|
634
|
+
customFooterItems.push(...itemsArray);
|
|
635
|
+
},
|
|
636
|
+
update: () => {
|
|
637
|
+
setUpdateCounter((c) => c + 1);
|
|
638
|
+
},
|
|
639
|
+
goBack: () => {
|
|
640
|
+
if (actions.back) {
|
|
641
|
+
actions.back();
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
close: (result) => {
|
|
645
|
+
cleanup(result);
|
|
646
|
+
},
|
|
647
|
+
parentData
|
|
648
|
+
};
|
|
649
|
+
if (!renderResult) {
|
|
650
|
+
renderResult = onRender(context);
|
|
651
|
+
}
|
|
652
|
+
useInput((input, key) => {
|
|
653
|
+
if (key.ctrl && input === "c") {
|
|
654
|
+
cleanup(null);
|
|
655
|
+
process.exit(0);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
let matchedBinding = null;
|
|
659
|
+
for (const binding of keyBindings) {
|
|
660
|
+
let keyMatches = false;
|
|
661
|
+
if (key[binding.key]) {
|
|
662
|
+
keyMatches = true;
|
|
663
|
+
} else if (input === binding.key) {
|
|
664
|
+
keyMatches = true;
|
|
665
|
+
} else if (key?.name === binding.key) {
|
|
666
|
+
keyMatches = true;
|
|
667
|
+
}
|
|
668
|
+
if (keyMatches) {
|
|
669
|
+
if (binding.enabled === false) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (binding.condition && !binding.condition(context)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
matchedBinding = binding;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (matchedBinding && actions[matchedBinding.action]) {
|
|
680
|
+
const actionResult = actions[matchedBinding.action]({
|
|
681
|
+
input,
|
|
682
|
+
key,
|
|
683
|
+
binding: matchedBinding
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
const footerLines = [];
|
|
688
|
+
const bindingItems = formatKeyBindings(keyBindings, "long");
|
|
689
|
+
if (bindingItems.length > 0) {
|
|
690
|
+
const bindingsLine = [];
|
|
691
|
+
bindingItems.forEach((item, idx) => {
|
|
692
|
+
if (idx > 0) {
|
|
693
|
+
bindingsLine.push(", ");
|
|
694
|
+
}
|
|
695
|
+
bindingsLine.push(item);
|
|
696
|
+
});
|
|
697
|
+
const allStrings = bindingItems.every((item) => typeof item === "string");
|
|
698
|
+
if (allStrings) {
|
|
699
|
+
footerLines.push(bindingsLine.join(""));
|
|
700
|
+
} else {
|
|
701
|
+
const wrappedBindingsLine = bindingsLine.map(
|
|
702
|
+
(item) => typeof item === "string" ? h3(Text3, {}, item) : item
|
|
703
|
+
);
|
|
704
|
+
footerLines.push(wrappedBindingsLine);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
customFooterItems.forEach((item) => {
|
|
708
|
+
if (typeof item === "string") {
|
|
709
|
+
footerLines.push(item);
|
|
710
|
+
} else {
|
|
711
|
+
footerLines.push(item);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return h3(
|
|
715
|
+
ScreenContainer,
|
|
716
|
+
{},
|
|
717
|
+
h3(ScreenTitle, { text: title }),
|
|
718
|
+
h3(ScreenDivider),
|
|
719
|
+
h3(ScreenRow, {}, h3(Text3, {}, " ")),
|
|
720
|
+
renderResult,
|
|
721
|
+
h3(ScreenRow, {}, h3(Text3, {}, " ")),
|
|
722
|
+
h3(ScreenDivider),
|
|
723
|
+
h3(ScreenFooter, { lines: footerLines })
|
|
724
|
+
);
|
|
725
|
+
};
|
|
726
|
+
const cleanup = (result) => {
|
|
727
|
+
if (instance) instance.unmount();
|
|
728
|
+
setTimeout(() => resolve2(result), 50);
|
|
729
|
+
};
|
|
730
|
+
instance = render(h3(Screen));
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async function showListScreen(config2) {
|
|
734
|
+
const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker } = config2;
|
|
735
|
+
return showScreen({
|
|
736
|
+
title,
|
|
737
|
+
parentData,
|
|
738
|
+
onRender: (ctx) => {
|
|
739
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
740
|
+
ctx.setAction("select", () => {
|
|
741
|
+
const selected = items[selectedIndexRef.current];
|
|
742
|
+
if (onSelect) {
|
|
743
|
+
const result = onSelect(selected.value, selectedIndexRef.current);
|
|
744
|
+
ctx.close(result);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
if (onEscape) {
|
|
748
|
+
ctx.setAction("back", () => {
|
|
749
|
+
const result = onEscape(selectedIndexRef.current);
|
|
750
|
+
ctx.close(result);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
754
|
+
return h3(ListComponent, { items, ctx, selectedIndexRef, renderItem, getTitle, sortable, maxHeight, sortHighlightStyle, selectionMarker });
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
async function showMultiColumnListScreen(config2) {
|
|
759
|
+
const { title, items, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
|
|
760
|
+
return showScreen({
|
|
761
|
+
title,
|
|
762
|
+
parentData,
|
|
763
|
+
onRender: (ctx) => {
|
|
764
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
765
|
+
ctx.setAction("select", () => {
|
|
766
|
+
const selected = items[selectedIndexRef.current];
|
|
767
|
+
if (onSelect) {
|
|
768
|
+
const result = onSelect(selected, selectedIndexRef.current);
|
|
769
|
+
ctx.close(result);
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
if (onEscape) {
|
|
773
|
+
ctx.setAction("back", () => {
|
|
774
|
+
const result = onEscape(selectedIndexRef.current);
|
|
775
|
+
ctx.close(result);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
779
|
+
return h3(MultiColumnListComponent, { items, ctx, selectedIndexRef });
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async function showMultiColumnListWithPreviewScreen(config2) {
|
|
784
|
+
const { title, items, getPreviewContent, onSelect, onEscape, parentData, initialSelectedIndex = 0 } = config2;
|
|
785
|
+
return showScreen({
|
|
786
|
+
title,
|
|
787
|
+
parentData,
|
|
788
|
+
onRender: (ctx) => {
|
|
789
|
+
const selectedIndexRef = { current: initialSelectedIndex };
|
|
790
|
+
ctx.setAction("select", () => {
|
|
791
|
+
const selected = items[selectedIndexRef.current];
|
|
792
|
+
if (onSelect) {
|
|
793
|
+
const result = onSelect(selected, selectedIndexRef.current);
|
|
794
|
+
ctx.close(result);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
if (onEscape) {
|
|
798
|
+
ctx.setAction("back", () => {
|
|
799
|
+
const result = onEscape(selectedIndexRef.current);
|
|
800
|
+
ctx.close(result);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
ctx.setKeyBinding({ key: "return", caption: "select", action: "select", order: 2 });
|
|
804
|
+
return h3(MultiColumnListWithPreviewComponent, { items, getPreviewContent, ctx, selectedIndexRef });
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
var showMenuScreen, showWordGridScreen;
|
|
809
|
+
var init_screens = __esm({
|
|
810
|
+
"src/screen/screens.ts"() {
|
|
811
|
+
"use strict";
|
|
812
|
+
init_components();
|
|
813
|
+
init_list_components();
|
|
814
|
+
showMenuScreen = showListScreen;
|
|
815
|
+
showWordGridScreen = showMultiColumnListScreen;
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// src/screen/ui-elements.ts
|
|
820
|
+
import { createElement as h4 } from "react";
|
|
821
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
822
|
+
function ListItem({
|
|
823
|
+
children,
|
|
824
|
+
isSelected = false,
|
|
825
|
+
color = "white",
|
|
826
|
+
backgroundColor,
|
|
827
|
+
bold = false,
|
|
828
|
+
dimColor = false
|
|
829
|
+
}) {
|
|
830
|
+
return h4(
|
|
831
|
+
Box4,
|
|
832
|
+
{},
|
|
833
|
+
h4(Text4, {
|
|
834
|
+
color: isSelected ? backgroundColor || "green" : color,
|
|
835
|
+
backgroundColor: isSelected ? color : backgroundColor,
|
|
836
|
+
bold: isSelected || bold,
|
|
837
|
+
dimColor: !isSelected && dimColor
|
|
838
|
+
}, children)
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
function TextBlock({
|
|
842
|
+
text,
|
|
843
|
+
color = "white",
|
|
844
|
+
dimmed = false,
|
|
845
|
+
bold = false,
|
|
846
|
+
maxWidth
|
|
847
|
+
}) {
|
|
848
|
+
return h4(
|
|
849
|
+
Box4,
|
|
850
|
+
{},
|
|
851
|
+
h4(Text4, {
|
|
852
|
+
color,
|
|
853
|
+
dimColor: dimmed,
|
|
854
|
+
bold
|
|
855
|
+
}, text)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
function Divider({ character = "\u2500", width = 80 }) {
|
|
859
|
+
return h4(
|
|
860
|
+
Box4,
|
|
861
|
+
{ marginY: 1 },
|
|
862
|
+
h4(Text4, { dimColor: true }, character.repeat(width))
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
function GridCell({
|
|
866
|
+
children,
|
|
867
|
+
width,
|
|
868
|
+
color = "white",
|
|
869
|
+
backgroundColor,
|
|
870
|
+
bold = false,
|
|
871
|
+
dimColor = false,
|
|
872
|
+
align = "left"
|
|
873
|
+
}) {
|
|
874
|
+
return h4(
|
|
875
|
+
Box4,
|
|
876
|
+
{ width },
|
|
877
|
+
h4(Text4, {
|
|
878
|
+
color,
|
|
879
|
+
backgroundColor,
|
|
880
|
+
bold,
|
|
881
|
+
dimColor,
|
|
882
|
+
textAlign: align
|
|
883
|
+
}, children)
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
887
|
+
return h4(
|
|
888
|
+
Box4,
|
|
889
|
+
{ flexDirection: "column" },
|
|
890
|
+
h4(Text4, {}, prompt),
|
|
891
|
+
h4(
|
|
892
|
+
Box4,
|
|
893
|
+
{ marginTop: 1 },
|
|
894
|
+
h4(Text4, { color: "cyan" }, " > ", value, "_")
|
|
895
|
+
)
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
var init_ui_elements = __esm({
|
|
899
|
+
"src/screen/ui-elements.ts"() {
|
|
900
|
+
"use strict";
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// src/screen/utils.ts
|
|
905
|
+
function buildBreadcrumb(parts) {
|
|
906
|
+
if (parts.length === 0) return "";
|
|
907
|
+
if (parts.length === 1) return parts[0];
|
|
908
|
+
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
909
|
+
}
|
|
910
|
+
function buildDetailBreadcrumb(path5, suffix = "") {
|
|
911
|
+
if (path5.length <= 1) {
|
|
912
|
+
return suffix ? `\u2190 ${suffix}` : path5[0] || "";
|
|
913
|
+
}
|
|
914
|
+
const breadcrumb = buildBreadcrumb(path5);
|
|
915
|
+
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
916
|
+
}
|
|
917
|
+
var init_utils = __esm({
|
|
918
|
+
"src/screen/utils.ts"() {
|
|
919
|
+
"use strict";
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
// src/screen/footer-builder.ts
|
|
924
|
+
function buildFooter(config2 = {}) {
|
|
925
|
+
const {
|
|
926
|
+
navigation = null,
|
|
927
|
+
actions = null,
|
|
928
|
+
info = null,
|
|
929
|
+
escape = "Esc to go back",
|
|
930
|
+
custom = null
|
|
931
|
+
} = config2;
|
|
932
|
+
const lines = [];
|
|
933
|
+
const mainParts = [];
|
|
934
|
+
if (navigation) {
|
|
935
|
+
mainParts.push(navigation);
|
|
936
|
+
}
|
|
937
|
+
if (actions) {
|
|
938
|
+
mainParts.push(actions);
|
|
939
|
+
}
|
|
940
|
+
if (escape) {
|
|
941
|
+
mainParts.push(escape);
|
|
942
|
+
}
|
|
943
|
+
if (mainParts.length > 0) {
|
|
944
|
+
lines.push(mainParts.join(", "));
|
|
945
|
+
}
|
|
946
|
+
if (info) {
|
|
947
|
+
const infoLines = Array.isArray(info) ? info : [info];
|
|
948
|
+
lines.push(...infoLines);
|
|
949
|
+
}
|
|
950
|
+
if (custom) {
|
|
951
|
+
const customLines = Array.isArray(custom) ? custom : [custom];
|
|
952
|
+
lines.push(...customLines);
|
|
953
|
+
}
|
|
954
|
+
return lines;
|
|
955
|
+
}
|
|
956
|
+
function organizeFooterMessages(messages) {
|
|
957
|
+
if (!messages || messages.length === 0) {
|
|
958
|
+
return ["Esc to go back"];
|
|
959
|
+
}
|
|
960
|
+
const navigation = messages.filter((m) => m.includes("\u2191") || m.includes("\u2193") || m.includes("\u2190") || m.includes("\u2192"));
|
|
961
|
+
const actions = messages.filter((m) => m.includes("Enter") || m.includes("select") || m.includes("submit"));
|
|
962
|
+
const escape = messages.filter((m) => m.includes("Esc"));
|
|
963
|
+
const others = messages.filter(
|
|
964
|
+
(m) => !navigation.includes(m) && !actions.includes(m) && !escape.includes(m)
|
|
965
|
+
);
|
|
966
|
+
const lines = [];
|
|
967
|
+
const mainLine = [...navigation, ...actions, ...escape].join(", ");
|
|
968
|
+
if (mainLine) lines.push(mainLine);
|
|
969
|
+
lines.push(...others);
|
|
970
|
+
return lines;
|
|
971
|
+
}
|
|
972
|
+
var FooterPresets;
|
|
973
|
+
var init_footer_builder = __esm({
|
|
974
|
+
"src/screen/footer-builder.ts"() {
|
|
975
|
+
"use strict";
|
|
976
|
+
FooterPresets = {
|
|
977
|
+
/**
|
|
978
|
+
* Menu screen footer
|
|
979
|
+
*/
|
|
980
|
+
menu: (customInfo = null) => buildFooter({
|
|
981
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
982
|
+
actions: "Enter to select",
|
|
983
|
+
escape: "Esc to go back",
|
|
984
|
+
info: customInfo
|
|
985
|
+
}),
|
|
986
|
+
/**
|
|
987
|
+
* Word grid footer
|
|
988
|
+
*/
|
|
989
|
+
wordGrid: (totalWords) => buildFooter({
|
|
990
|
+
navigation: "\u2191\u2193\u2190\u2192 to navigate",
|
|
991
|
+
actions: "Enter to select",
|
|
992
|
+
escape: "Esc to go back",
|
|
993
|
+
info: `Total: ${totalWords} words`
|
|
994
|
+
}),
|
|
995
|
+
/**
|
|
996
|
+
* Text input footer
|
|
997
|
+
*/
|
|
998
|
+
textInput: () => buildFooter({
|
|
999
|
+
actions: "Type and press Enter to submit",
|
|
1000
|
+
escape: "Esc to cancel"
|
|
1001
|
+
}),
|
|
1002
|
+
/**
|
|
1003
|
+
* Info/static screen footer
|
|
1004
|
+
*/
|
|
1005
|
+
info: () => buildFooter({
|
|
1006
|
+
escape: "Esc to continue"
|
|
1007
|
+
}),
|
|
1008
|
+
/**
|
|
1009
|
+
* Main menu footer (escape exits)
|
|
1010
|
+
*/
|
|
1011
|
+
mainMenu: () => buildFooter({
|
|
1012
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
1013
|
+
actions: "Enter to select",
|
|
1014
|
+
escape: "Esc to exit"
|
|
1015
|
+
}),
|
|
1016
|
+
/**
|
|
1017
|
+
* Action menu footer (for word cards, etc.)
|
|
1018
|
+
*/
|
|
1019
|
+
actionMenu: (hasAudio = false) => {
|
|
1020
|
+
const parts = buildFooter({
|
|
1021
|
+
navigation: "\u2191/\u2193 to navigate",
|
|
1022
|
+
actions: "Enter to select",
|
|
1023
|
+
escape: "Esc to go back"
|
|
1024
|
+
});
|
|
1025
|
+
if (hasAudio) {
|
|
1026
|
+
parts.push("Audio available");
|
|
1027
|
+
}
|
|
1028
|
+
return parts;
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// src/screen/index.ts
|
|
1035
|
+
var screen_exports = {};
|
|
1036
|
+
__export(screen_exports, {
|
|
1037
|
+
Box: () => Box5,
|
|
1038
|
+
Divider: () => Divider,
|
|
1039
|
+
FooterPresets: () => FooterPresets,
|
|
1040
|
+
GridCell: () => GridCell,
|
|
1041
|
+
InputField: () => InputField,
|
|
1042
|
+
ListComponent: () => ListComponent,
|
|
1043
|
+
ListItem: () => ListItem,
|
|
1044
|
+
MultiColumnListComponent: () => MultiColumnListComponent,
|
|
1045
|
+
MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
|
|
1046
|
+
React: () => React5,
|
|
1047
|
+
ScreenBody: () => ScreenBody,
|
|
1048
|
+
ScreenContainer: () => ScreenContainer,
|
|
1049
|
+
ScreenDivider: () => ScreenDivider,
|
|
1050
|
+
ScreenFooter: () => ScreenFooter,
|
|
1051
|
+
ScreenRow: () => ScreenRow,
|
|
1052
|
+
ScreenTitle: () => ScreenTitle,
|
|
1053
|
+
Text: () => Text5,
|
|
1054
|
+
TextBlock: () => TextBlock,
|
|
1055
|
+
buildBreadcrumb: () => buildBreadcrumb,
|
|
1056
|
+
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
1057
|
+
buildFooter: () => buildFooter,
|
|
1058
|
+
h: () => createElement2,
|
|
1059
|
+
load: () => load,
|
|
1060
|
+
organizeFooterMessages: () => organizeFooterMessages,
|
|
1061
|
+
showListScreen: () => showListScreen,
|
|
1062
|
+
showMenuScreen: () => showMenuScreen,
|
|
1063
|
+
showMultiColumnListScreen: () => showMultiColumnListScreen,
|
|
1064
|
+
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
1065
|
+
showScreen: () => showScreen,
|
|
1066
|
+
showWordGridScreen: () => showWordGridScreen,
|
|
1067
|
+
useCallback: () => useCallback,
|
|
1068
|
+
useEffect: () => useEffect3,
|
|
1069
|
+
useInput: () => useInput2,
|
|
1070
|
+
useMemo: () => useMemo,
|
|
1071
|
+
useRef: () => useRef3,
|
|
1072
|
+
useState: () => useState3
|
|
1073
|
+
});
|
|
1074
|
+
import React5, { useState as useState3, useEffect as useEffect3, useRef as useRef3, useMemo, useCallback, createElement as createElement2 } from "react";
|
|
1075
|
+
import { Box as Box5, Text as Text5, useInput as useInput2 } from "ink";
|
|
1076
|
+
async function load() {
|
|
1077
|
+
if (loadPromise) return loadPromise;
|
|
1078
|
+
loadPromise = Promise.all([
|
|
1079
|
+
import("react"),
|
|
1080
|
+
import("ink")
|
|
1081
|
+
]).then(() => {
|
|
1082
|
+
});
|
|
1083
|
+
return loadPromise;
|
|
1084
|
+
}
|
|
1085
|
+
var loadPromise;
|
|
1086
|
+
var init_screen = __esm({
|
|
1087
|
+
"src/screen/index.ts"() {
|
|
1088
|
+
"use strict";
|
|
1089
|
+
init_screens();
|
|
1090
|
+
init_list_components();
|
|
1091
|
+
init_components();
|
|
1092
|
+
init_ui_elements();
|
|
1093
|
+
init_utils();
|
|
1094
|
+
init_footer_builder();
|
|
1095
|
+
loadPromise = null;
|
|
1096
|
+
if (typeof window === "undefined") {
|
|
1097
|
+
load().catch(() => {
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// src/scripts/cli-runner.ts
|
|
1104
|
+
import path4 from "path";
|
|
1105
|
+
import { pathToFileURL } from "url";
|
|
1106
|
+
|
|
1107
|
+
// src/args/index.ts
|
|
1108
|
+
import { readFileSync, existsSync } from "fs";
|
|
1109
|
+
import { resolve, dirname, basename, extname, join, isAbsolute } from "path";
|
|
1110
|
+
import { config } from "dotenv";
|
|
1111
|
+
var Args = class _Args {
|
|
1112
|
+
args = {};
|
|
1113
|
+
flags = {};
|
|
1114
|
+
options = {};
|
|
1115
|
+
commands = [];
|
|
1116
|
+
usedKeys = /* @__PURE__ */ new Set();
|
|
1117
|
+
aliases = {};
|
|
1118
|
+
overrides = {};
|
|
1119
|
+
defaults = {};
|
|
1120
|
+
prefixes = [];
|
|
1121
|
+
nots = [];
|
|
1122
|
+
configValues = {};
|
|
1123
|
+
configsLoaded = [];
|
|
1124
|
+
env = "local";
|
|
1125
|
+
constructor(contextOrConfig = {}, config2) {
|
|
1126
|
+
const hasContext = config2 !== void 0;
|
|
1127
|
+
const configToUse = hasContext ? config2 ?? {} : contextOrConfig ?? {};
|
|
1128
|
+
const context = hasContext ? contextOrConfig : void 0;
|
|
1129
|
+
this.aliases = {};
|
|
1130
|
+
this.overrides = {};
|
|
1131
|
+
this.defaults = {};
|
|
1132
|
+
this.prefixes = ["not", "no"];
|
|
1133
|
+
if (Object.keys(configToUse).length > 0) {
|
|
1134
|
+
this.configure(configToUse);
|
|
1135
|
+
}
|
|
1136
|
+
const args = configToUse.args || process.argv.slice(2);
|
|
1137
|
+
this.parseArgs(args);
|
|
1138
|
+
this.env = this.get("env")?.toLowerCase() || "local";
|
|
1139
|
+
this.loadDotEnv();
|
|
1140
|
+
this.loadConfigFiles();
|
|
1141
|
+
this.checkConflicts();
|
|
1142
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1143
|
+
context.registerCleanup((ctx) => {
|
|
1144
|
+
const unusedArgs = ctx.args.getUnused();
|
|
1145
|
+
if (unusedArgs.length > 0) {
|
|
1146
|
+
ctx.logger.warn("Unused CLI args:", unusedArgs.join(", "));
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Configure Args options
|
|
1153
|
+
* Only parameters present in config are updated
|
|
1154
|
+
* Note: Args is special - it's initialized first, so it can't take context
|
|
1155
|
+
*/
|
|
1156
|
+
configure(config2) {
|
|
1157
|
+
if (config2.aliases !== void 0) {
|
|
1158
|
+
this.aliases = config2.aliases;
|
|
1159
|
+
}
|
|
1160
|
+
if (config2.overrides !== void 0) {
|
|
1161
|
+
this.overrides = config2.overrides;
|
|
1162
|
+
}
|
|
1163
|
+
if (config2.defaults !== void 0) {
|
|
1164
|
+
this.defaults = config2.defaults;
|
|
1165
|
+
}
|
|
1166
|
+
if (config2.prefixes !== void 0) {
|
|
1167
|
+
this.prefixes = config2.prefixes;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Initialize Args instance.
|
|
1172
|
+
* Args.init(context, config) when used from init/setup: context has registerCleanup, Args registers unused-args cleanup.
|
|
1173
|
+
* Args.init(config) for standalone use (no cleanup).
|
|
1174
|
+
*/
|
|
1175
|
+
static init(contextOrConfig, config2) {
|
|
1176
|
+
if (config2 !== void 0) {
|
|
1177
|
+
return new _Args(contextOrConfig, config2);
|
|
1178
|
+
}
|
|
1179
|
+
return new _Args(contextOrConfig ?? {});
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Parse command line arguments
|
|
1183
|
+
*/
|
|
1184
|
+
parseArgs(args) {
|
|
1185
|
+
let i = 0;
|
|
1186
|
+
while (i < args.length) {
|
|
1187
|
+
const arg = args[i];
|
|
1188
|
+
if (arg.startsWith("--")) {
|
|
1189
|
+
const [key, value] = this.parseLongOption(arg);
|
|
1190
|
+
this.setValue(key, value);
|
|
1191
|
+
i++;
|
|
1192
|
+
} else if (arg.startsWith("-")) {
|
|
1193
|
+
const result = this.parseShortOption(arg, args, i);
|
|
1194
|
+
if (result.consumed > 0) {
|
|
1195
|
+
i += result.consumed;
|
|
1196
|
+
} else {
|
|
1197
|
+
i++;
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
this.commands.push(arg);
|
|
1201
|
+
i++;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Parse long option (--key=value or --key)
|
|
1207
|
+
*/
|
|
1208
|
+
parseLongOption(arg) {
|
|
1209
|
+
const key = arg.slice(2);
|
|
1210
|
+
const prefix = this.prefixes.find((p) => key.startsWith(p));
|
|
1211
|
+
if (prefix) {
|
|
1212
|
+
let strippedKey = key.slice(prefix.length);
|
|
1213
|
+
if (strippedKey.startsWith("-")) {
|
|
1214
|
+
strippedKey = strippedKey.slice(1);
|
|
1215
|
+
}
|
|
1216
|
+
this.nots.push(key);
|
|
1217
|
+
return [strippedKey, false];
|
|
1218
|
+
}
|
|
1219
|
+
if (key.includes("=")) {
|
|
1220
|
+
const eqIndex = key.indexOf("=");
|
|
1221
|
+
const optionKey = key.slice(0, eqIndex);
|
|
1222
|
+
const value = key.slice(eqIndex + 1);
|
|
1223
|
+
return [optionKey, this.parseValue(value)];
|
|
1224
|
+
} else {
|
|
1225
|
+
return [key, true];
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Parse short option (-k=value, -k, or bundled -vsd)
|
|
1230
|
+
*/
|
|
1231
|
+
parseShortOption(arg, args, index) {
|
|
1232
|
+
const key = arg.slice(1);
|
|
1233
|
+
if (key.length === 1 && index + 1 < args.length && !args[index + 1].startsWith("-")) {
|
|
1234
|
+
const value = args[index + 1];
|
|
1235
|
+
this.setValue(key, this.parseValue(value));
|
|
1236
|
+
return { consumed: 2 };
|
|
1237
|
+
}
|
|
1238
|
+
if (key.length > 1 && !key.includes("=")) {
|
|
1239
|
+
for (let i = 0; i < key.length; i++) {
|
|
1240
|
+
const shortKey = key[i];
|
|
1241
|
+
if (shortKey in this.aliases) {
|
|
1242
|
+
this.setValue(shortKey, true);
|
|
1243
|
+
} else {
|
|
1244
|
+
this.args[shortKey] = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return { consumed: 1 };
|
|
1248
|
+
}
|
|
1249
|
+
if (key.includes("=")) {
|
|
1250
|
+
const eqIndex = key.indexOf("=");
|
|
1251
|
+
const optionKey = key.slice(0, eqIndex);
|
|
1252
|
+
const value = key.slice(eqIndex + 1);
|
|
1253
|
+
if (optionKey.length > 1) {
|
|
1254
|
+
for (let i = 0; i < optionKey.length - 1; i++) {
|
|
1255
|
+
const shortKey = optionKey[i];
|
|
1256
|
+
if (shortKey in this.aliases) {
|
|
1257
|
+
this.setValue(shortKey, true);
|
|
1258
|
+
} else {
|
|
1259
|
+
this.args[shortKey] = true;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const lastKey = optionKey[optionKey.length - 1];
|
|
1263
|
+
if (lastKey in this.aliases) {
|
|
1264
|
+
this.setValue(lastKey, this.parseValue(value));
|
|
1265
|
+
} else {
|
|
1266
|
+
this.args[lastKey] = this.parseValue(value);
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
this.setValue(optionKey, this.parseValue(value));
|
|
1270
|
+
}
|
|
1271
|
+
return { consumed: 1 };
|
|
1272
|
+
} else {
|
|
1273
|
+
this.setValue(key, true);
|
|
1274
|
+
return { consumed: 1 };
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Parse value (handle quotes)
|
|
1279
|
+
*/
|
|
1280
|
+
parseValue(value) {
|
|
1281
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1282
|
+
return value.slice(1, -1);
|
|
1283
|
+
}
|
|
1284
|
+
return value;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Set a value with proper categorization
|
|
1288
|
+
*/
|
|
1289
|
+
setValue(key, value) {
|
|
1290
|
+
const resolvedKey = this.aliases[key] || key;
|
|
1291
|
+
if (typeof value === "boolean") {
|
|
1292
|
+
this.flags[resolvedKey] = value;
|
|
1293
|
+
} else {
|
|
1294
|
+
this.options[resolvedKey] = value;
|
|
1295
|
+
}
|
|
1296
|
+
this.args[resolvedKey.toLowerCase()] = value;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Check for conflicts (short + long form of same option)
|
|
1300
|
+
*/
|
|
1301
|
+
checkConflicts() {
|
|
1302
|
+
const conflicts = [];
|
|
1303
|
+
for (const [shortKey, longKey] of Object.entries(this.aliases)) {
|
|
1304
|
+
const hasShort = this.args[shortKey] !== void 0;
|
|
1305
|
+
const hasLong = this.args[longKey] !== void 0;
|
|
1306
|
+
if (hasShort && hasLong) {
|
|
1307
|
+
conflicts.push(`Both -${shortKey} and --${longKey} specified`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (conflicts.length > 0) {
|
|
1311
|
+
throw new Error(`Argument conflicts: ${conflicts.join(", ")}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get a value with precedence order
|
|
1316
|
+
*/
|
|
1317
|
+
get(key) {
|
|
1318
|
+
const resolvedKey = this.aliases[key] || key;
|
|
1319
|
+
const lcKey = resolvedKey.toLowerCase();
|
|
1320
|
+
this.usedKeys.add(lcKey);
|
|
1321
|
+
const overrideKey = Object.keys(this.overrides).find((k) => k.toLowerCase() === lcKey);
|
|
1322
|
+
if (overrideKey !== void 0) {
|
|
1323
|
+
return this.overrides[overrideKey];
|
|
1324
|
+
}
|
|
1325
|
+
const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
|
|
1326
|
+
if (this.env && this.args[lcKeyWithEnv] !== void 0) {
|
|
1327
|
+
return this.args[lcKeyWithEnv];
|
|
1328
|
+
} else if (this.args[lcKey] !== void 0) {
|
|
1329
|
+
return this.args[lcKey];
|
|
1330
|
+
}
|
|
1331
|
+
const configKey = Object.keys(this.configValues).find((k) => k.toLowerCase() === lcKey);
|
|
1332
|
+
if (configKey !== void 0) {
|
|
1333
|
+
return this.configValues[configKey];
|
|
1334
|
+
}
|
|
1335
|
+
const envKey = this.toEnvKey(resolvedKey);
|
|
1336
|
+
const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
|
|
1337
|
+
const envSpecificKey = Object.keys(process.env).find(
|
|
1338
|
+
(k) => this.env && k.toUpperCase() === envKeyWithEnv
|
|
1339
|
+
);
|
|
1340
|
+
const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
|
|
1341
|
+
const envKeyAlt = envKey.replace(/_([0-9])/g, "$1");
|
|
1342
|
+
const envKeyAltFound = !envKeyFound ? Object.keys(process.env).find((k) => k.toUpperCase() === envKeyAlt) : null;
|
|
1343
|
+
if (envSpecificKey) {
|
|
1344
|
+
return process.env[envSpecificKey];
|
|
1345
|
+
} else if (envKeyFound) {
|
|
1346
|
+
return process.env[envKeyFound];
|
|
1347
|
+
} else if (envKeyAltFound) {
|
|
1348
|
+
return process.env[envKeyAltFound];
|
|
1349
|
+
}
|
|
1350
|
+
const defaultKey = Object.keys(this.defaults).find((k) => k.toLowerCase() === lcKey);
|
|
1351
|
+
if (defaultKey !== void 0) {
|
|
1352
|
+
return this.defaults[defaultKey];
|
|
1353
|
+
}
|
|
1354
|
+
if (lcKey === "env" && process.env.NODE_ENV !== void 0) {
|
|
1355
|
+
return process.env.NODE_ENV;
|
|
1356
|
+
}
|
|
1357
|
+
return void 0;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Return which layer provided the value for get(key): overrides, cli, config, env, or default.
|
|
1361
|
+
* Does not add key to usedKeys. Use after get(key) when you need the origin.
|
|
1362
|
+
*/
|
|
1363
|
+
getSource(key) {
|
|
1364
|
+
const resolvedKey = this.aliases[key] || key;
|
|
1365
|
+
const lcKey = resolvedKey.toLowerCase();
|
|
1366
|
+
const overrideKey = Object.keys(this.overrides).find((k) => k.toLowerCase() === lcKey);
|
|
1367
|
+
if (overrideKey !== void 0) return "overrides";
|
|
1368
|
+
const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
|
|
1369
|
+
if (this.env && this.args[lcKeyWithEnv] !== void 0) return "cli";
|
|
1370
|
+
if (this.args[lcKey] !== void 0) return "cli";
|
|
1371
|
+
const configKey = Object.keys(this.configValues).find((k) => k.toLowerCase() === lcKey);
|
|
1372
|
+
if (configKey !== void 0) return "config";
|
|
1373
|
+
const envKey = this.toEnvKey(resolvedKey);
|
|
1374
|
+
const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
|
|
1375
|
+
const envSpecificKey = Object.keys(process.env).find((k) => this.env && k.toUpperCase() === envKeyWithEnv);
|
|
1376
|
+
const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
|
|
1377
|
+
const envKeyAlt = envKey.replace(/_([0-9])/g, "$1");
|
|
1378
|
+
const envKeyAltFound = !envKeyFound ? Object.keys(process.env).find((k) => k.toUpperCase() === envKeyAlt) : null;
|
|
1379
|
+
if (envSpecificKey || envKeyFound || envKeyAltFound) return "env";
|
|
1380
|
+
const defaultKey = Object.keys(this.defaults).find((k) => k.toLowerCase() === lcKey);
|
|
1381
|
+
if (defaultKey !== void 0) return "default";
|
|
1382
|
+
if (lcKey === "env" && process.env.NODE_ENV !== void 0) return "env";
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Set a value (for testing/internal use)
|
|
1387
|
+
*/
|
|
1388
|
+
set(key, value) {
|
|
1389
|
+
this.args[key] = value;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Check if a command exists (case-insensitive)
|
|
1393
|
+
*/
|
|
1394
|
+
hasCommand(cmd) {
|
|
1395
|
+
return this.commands.some((command) => command.toLowerCase() === cmd.toLowerCase());
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get all commands
|
|
1399
|
+
*/
|
|
1400
|
+
getCommands() {
|
|
1401
|
+
return [...this.commands];
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Get used keys (as array)
|
|
1405
|
+
*/
|
|
1406
|
+
getUsed() {
|
|
1407
|
+
return Array.from(this.usedKeys);
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Get unused keys (as array)
|
|
1411
|
+
*/
|
|
1412
|
+
getUnused() {
|
|
1413
|
+
const unused = [];
|
|
1414
|
+
for (const key of Object.keys(this.args)) {
|
|
1415
|
+
if (!this.usedKeys.has(key) && !this.nots.includes(key)) {
|
|
1416
|
+
unused.push(key);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return unused;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Convert key to environment variable format
|
|
1423
|
+
* Converts camelCase to SNAKE_CASE
|
|
1424
|
+
*/
|
|
1425
|
+
toEnvKey(key) {
|
|
1426
|
+
if (key.includes("_") && key === key.toUpperCase()) {
|
|
1427
|
+
return key;
|
|
1428
|
+
}
|
|
1429
|
+
return key.replace(
|
|
1430
|
+
/[A-Z0-9]/g,
|
|
1431
|
+
(match, offset) => offset === 0 ? match : "_" + match.toLowerCase()
|
|
1432
|
+
).toUpperCase();
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Load .env file
|
|
1436
|
+
*/
|
|
1437
|
+
loadDotEnv() {
|
|
1438
|
+
const dotEnvPath = this.get("dotEnvPath") || process.cwd();
|
|
1439
|
+
const dotEnvFile = this.get("dotEnvFile") || ".env";
|
|
1440
|
+
if (this.get("dotEnvFile")) {
|
|
1441
|
+
const customPath = resolve(dotEnvPath, dotEnvFile);
|
|
1442
|
+
if (existsSync(customPath)) {
|
|
1443
|
+
config({ path: customPath, quiet: true });
|
|
1444
|
+
}
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
let dotEnvPathFile = null;
|
|
1448
|
+
const envSpecificFile = `.env.${this.env}`;
|
|
1449
|
+
const envSpecificPath = resolve(dotEnvPath, envSpecificFile);
|
|
1450
|
+
if (existsSync(envSpecificPath)) {
|
|
1451
|
+
dotEnvPathFile = envSpecificPath;
|
|
1452
|
+
}
|
|
1453
|
+
if (!dotEnvPathFile && !this.get("dotEnvPath")) {
|
|
1454
|
+
const examplesPath = resolve(dotEnvPath, "examples");
|
|
1455
|
+
const examplesEnvSpecificPath = resolve(examplesPath, envSpecificFile);
|
|
1456
|
+
if (existsSync(examplesEnvSpecificPath)) {
|
|
1457
|
+
dotEnvPathFile = examplesEnvSpecificPath;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!dotEnvPathFile) {
|
|
1461
|
+
dotEnvPathFile = resolve(dotEnvPath, dotEnvFile);
|
|
1462
|
+
if (!existsSync(dotEnvPathFile)) {
|
|
1463
|
+
if (!this.get("dotEnvPath")) {
|
|
1464
|
+
const examplesPath = resolve(dotEnvPath, "examples");
|
|
1465
|
+
const examplesEnvFile = resolve(examplesPath, dotEnvFile);
|
|
1466
|
+
if (existsSync(examplesEnvFile)) {
|
|
1467
|
+
dotEnvPathFile = examplesEnvFile;
|
|
1468
|
+
} else {
|
|
1469
|
+
dotEnvPathFile = resolve(dotEnvPath, "..", dotEnvFile);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (dotEnvPathFile && existsSync(dotEnvPathFile)) {
|
|
1475
|
+
config({ path: dotEnvPathFile, quiet: true });
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Load configuration files
|
|
1480
|
+
*/
|
|
1481
|
+
loadConfigFiles() {
|
|
1482
|
+
this.configsLoaded = [];
|
|
1483
|
+
this.configValues = {};
|
|
1484
|
+
const _defaultConfigExtension = this.get("defaultConfigExtension") || "js";
|
|
1485
|
+
const optConfigFiles = this.get("config") || this.get("configs") || "";
|
|
1486
|
+
const configFiles = optConfigFiles ? optConfigFiles.split(/,\s*/) : [];
|
|
1487
|
+
const optConfigFilePath = this.get("configPath");
|
|
1488
|
+
if (configFiles.length > 0) {
|
|
1489
|
+
for (const cfgFile of configFiles) {
|
|
1490
|
+
let notLoaded = false;
|
|
1491
|
+
let notLoadedEnvSpecific = false;
|
|
1492
|
+
const cfgFileWithPath = this.resolveFileWithPath(optConfigFilePath, cfgFile);
|
|
1493
|
+
try {
|
|
1494
|
+
const cfgContents = this.requireConfigFile(cfgFileWithPath);
|
|
1495
|
+
this.configValues = { ...this.configValues, ...cfgContents };
|
|
1496
|
+
this.configsLoaded.push(cfgFileWithPath);
|
|
1497
|
+
} catch {
|
|
1498
|
+
notLoaded = true;
|
|
1499
|
+
}
|
|
1500
|
+
const cfgEnvFileWithPath = this.resolveFileWithPath(
|
|
1501
|
+
optConfigFilePath,
|
|
1502
|
+
cfgFile,
|
|
1503
|
+
this.env
|
|
1504
|
+
);
|
|
1505
|
+
if (cfgEnvFileWithPath !== cfgFileWithPath) {
|
|
1506
|
+
try {
|
|
1507
|
+
const cfgContents = this.requireConfigFile(cfgEnvFileWithPath);
|
|
1508
|
+
this.configValues = { ...this.configValues, ...cfgContents };
|
|
1509
|
+
this.configsLoaded.push(cfgEnvFileWithPath);
|
|
1510
|
+
} catch {
|
|
1511
|
+
notLoadedEnvSpecific = true;
|
|
1512
|
+
}
|
|
1513
|
+
} else {
|
|
1514
|
+
notLoadedEnvSpecific = true;
|
|
1515
|
+
}
|
|
1516
|
+
if (notLoaded && notLoadedEnvSpecific) {
|
|
1517
|
+
throw new Error(`can't load config file "${cfgFileWithPath}"`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Resolve file path with environment-specific naming
|
|
1524
|
+
*/
|
|
1525
|
+
resolveFileWithPath(optConfigFilePath, cfgFile, env) {
|
|
1526
|
+
let cfgFileWithPath = optConfigFilePath ? isAbsolute(optConfigFilePath) ? resolve(optConfigFilePath, cfgFile) : resolve(process.cwd(), optConfigFilePath, cfgFile) : isAbsolute(cfgFile) ? cfgFile : resolve(process.cwd(), cfgFile);
|
|
1527
|
+
const { basePathWithName, extension } = this.splitPath(cfgFileWithPath);
|
|
1528
|
+
if (env) {
|
|
1529
|
+
cfgFileWithPath = `${basePathWithName}.${env}.${extension || "js"}`;
|
|
1530
|
+
} else {
|
|
1531
|
+
cfgFileWithPath = `${basePathWithName}.${extension || "js"}`;
|
|
1532
|
+
}
|
|
1533
|
+
return cfgFileWithPath;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Split file path into base path and extension
|
|
1537
|
+
*/
|
|
1538
|
+
splitPath(filePath) {
|
|
1539
|
+
const basePathWithName = join(dirname(filePath), basename(filePath, extname(filePath)));
|
|
1540
|
+
const extension = extname(filePath).slice(1);
|
|
1541
|
+
return { basePathWithName, extension };
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Require a configuration file (supports .js and .json)
|
|
1545
|
+
*/
|
|
1546
|
+
requireConfigFile(filePath) {
|
|
1547
|
+
if (!existsSync(filePath)) {
|
|
1548
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
1549
|
+
}
|
|
1550
|
+
const ext = extname(filePath).toLowerCase();
|
|
1551
|
+
if (ext === ".json") {
|
|
1552
|
+
const content = readFileSync(filePath, "utf8");
|
|
1553
|
+
return JSON.parse(content);
|
|
1554
|
+
} else if (ext === ".js") {
|
|
1555
|
+
try {
|
|
1556
|
+
delete __require.cache[__require.resolve(filePath)];
|
|
1557
|
+
return __require(filePath);
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
throw new Error(`Failed to load JS config file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
throw new Error(`Unsupported file extension: ${ext}`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Get all parsed data
|
|
1567
|
+
*/
|
|
1568
|
+
getParsed() {
|
|
1569
|
+
return {
|
|
1570
|
+
command: this.commands[0] || "",
|
|
1571
|
+
flags: { ...this.flags },
|
|
1572
|
+
options: { ...this.options },
|
|
1573
|
+
usedKeys: Array.from(this.usedKeys)
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Set prefixes dynamically and re-parse arguments (like legacy)
|
|
1578
|
+
*/
|
|
1579
|
+
setPrefixes(prefixes) {
|
|
1580
|
+
const arr = Array.isArray(prefixes) ? prefixes : prefixes.split(/,\s*/);
|
|
1581
|
+
const sortedArr = arr.sort(
|
|
1582
|
+
(a, b) => a.length < b.length ? 1 : a.length > b.length ? -1 : 0
|
|
1583
|
+
);
|
|
1584
|
+
this.prefixes = sortedArr.map((el) => el.toLowerCase());
|
|
1585
|
+
const args = process.argv.slice(2);
|
|
1586
|
+
this.parseArgs(args);
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
// src/params/index.ts
|
|
1591
|
+
import Joi from "joi";
|
|
1592
|
+
|
|
1593
|
+
// src/errors.ts
|
|
1594
|
+
var FrameworkError = class extends Error {
|
|
1595
|
+
constructor(message) {
|
|
1596
|
+
super(message);
|
|
1597
|
+
this.name = "FrameworkError";
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
var ParamError = class extends FrameworkError {
|
|
1601
|
+
constructor(message) {
|
|
1602
|
+
super(message);
|
|
1603
|
+
this.name = "ParamError";
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
var InitError = class extends FrameworkError {
|
|
1607
|
+
constructor(message) {
|
|
1608
|
+
super(message);
|
|
1609
|
+
this.name = "InitError";
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
var FileDatabaseError = class extends FrameworkError {
|
|
1613
|
+
constructor(message) {
|
|
1614
|
+
super(message);
|
|
1615
|
+
this.name = "FileDatabaseError";
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
// src/params/custom-types.ts
|
|
1620
|
+
var joiEdateType = (value, helpers) => {
|
|
1621
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
1622
|
+
const testDate = new Date(value);
|
|
1623
|
+
if (!isNaN(testDate.getTime())) {
|
|
1624
|
+
return value;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (value instanceof Date) {
|
|
1628
|
+
return value.toISOString();
|
|
1629
|
+
}
|
|
1630
|
+
if (typeof value !== "string") {
|
|
1631
|
+
value = String(value);
|
|
1632
|
+
}
|
|
1633
|
+
if (value.toLowerCase() === "now") {
|
|
1634
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1635
|
+
}
|
|
1636
|
+
const referenceRegex = /^@(\w+)([+-]\d+[smhdwy])$/i;
|
|
1637
|
+
const referenceMatch = value.match(referenceRegex);
|
|
1638
|
+
if (referenceMatch) {
|
|
1639
|
+
const [, paramName, relativeExpr] = referenceMatch;
|
|
1640
|
+
const context = helpers.prefs?.context;
|
|
1641
|
+
if (!context || !context.params) {
|
|
1642
|
+
throw new ParamError(`Cannot resolve cross-parameter reference @${paramName}: context not available. Ensure parameters are processed with proper context.`);
|
|
1643
|
+
}
|
|
1644
|
+
const referencedValue = context.params[paramName];
|
|
1645
|
+
if (referencedValue === void 0 || referencedValue === null) {
|
|
1646
|
+
throw new ParamError(`Cannot resolve @${paramName}: parameter "${paramName}" is not defined or has no value. Parameters are evaluated left-to-right.`);
|
|
1647
|
+
}
|
|
1648
|
+
let referenceDate;
|
|
1649
|
+
if (referencedValue instanceof Date) {
|
|
1650
|
+
referenceDate = referencedValue;
|
|
1651
|
+
} else if (typeof referencedValue === "string") {
|
|
1652
|
+
referenceDate = new Date(referencedValue);
|
|
1653
|
+
if (isNaN(referenceDate.getTime())) {
|
|
1654
|
+
throw new ParamError(`Referenced parameter @${paramName} has invalid date value: ${referencedValue}`);
|
|
1655
|
+
}
|
|
1656
|
+
} else {
|
|
1657
|
+
throw new ParamError(`Referenced parameter @${paramName} is not a valid date type (found: ${typeof referencedValue})`);
|
|
1658
|
+
}
|
|
1659
|
+
const relativeMatch2 = relativeExpr.match(/^([+-])(\d+)([smhdwy])$/i);
|
|
1660
|
+
if (!relativeMatch2) {
|
|
1661
|
+
throw new ParamError(`Invalid relative time expression in @${paramName}${relativeExpr}`);
|
|
1662
|
+
}
|
|
1663
|
+
const [, sign, amount, unit] = relativeMatch2;
|
|
1664
|
+
const offset = calculateTimeOffset(parseInt(amount, 10), unit, sign);
|
|
1665
|
+
const resultDate = new Date(referenceDate.getTime() + offset);
|
|
1666
|
+
return resultDate.toISOString();
|
|
1667
|
+
}
|
|
1668
|
+
const relativeTimeRegex = /^([+-])(\d+)([smhdwy])$/i;
|
|
1669
|
+
const relativeMatch = value.match(relativeTimeRegex);
|
|
1670
|
+
if (relativeMatch) {
|
|
1671
|
+
const [, sign, amount, unit] = relativeMatch;
|
|
1672
|
+
const numAmount = parseInt(amount, 10);
|
|
1673
|
+
if (isNaN(numAmount)) {
|
|
1674
|
+
throw new ParamError(`Invalid relative time amount: ${amount}`);
|
|
1675
|
+
}
|
|
1676
|
+
const offset = calculateTimeOffset(numAmount, unit, sign);
|
|
1677
|
+
const resultDate = new Date(Date.now() + offset);
|
|
1678
|
+
return resultDate.toISOString();
|
|
1679
|
+
}
|
|
1680
|
+
const parsedDate = new Date(value);
|
|
1681
|
+
if (isNaN(parsedDate.getTime())) {
|
|
1682
|
+
throw new ParamError(`Invalid date format: ${value}. Expected a valid date string, "now", relative time expression (e.g., "-2h", "+1d"), or cross-parameter reference (e.g., "@startTime+2h")`);
|
|
1683
|
+
}
|
|
1684
|
+
return parsedDate.toISOString();
|
|
1685
|
+
};
|
|
1686
|
+
function calculateTimeOffset(amount, unit, sign) {
|
|
1687
|
+
let multiplier = 1;
|
|
1688
|
+
switch (unit.toLowerCase()) {
|
|
1689
|
+
case "s":
|
|
1690
|
+
multiplier = 1e3;
|
|
1691
|
+
break;
|
|
1692
|
+
case "m":
|
|
1693
|
+
multiplier = 60 * 1e3;
|
|
1694
|
+
break;
|
|
1695
|
+
case "h":
|
|
1696
|
+
multiplier = 60 * 60 * 1e3;
|
|
1697
|
+
break;
|
|
1698
|
+
case "d":
|
|
1699
|
+
multiplier = 24 * 60 * 60 * 1e3;
|
|
1700
|
+
break;
|
|
1701
|
+
case "w":
|
|
1702
|
+
multiplier = 7 * 24 * 60 * 60 * 1e3;
|
|
1703
|
+
break;
|
|
1704
|
+
case "y":
|
|
1705
|
+
multiplier = 365 * 24 * 60 * 60 * 1e3;
|
|
1706
|
+
break;
|
|
1707
|
+
default:
|
|
1708
|
+
throw new ParamError(`Invalid time unit: ${unit}. Supported units: s, m, h, d, w, y`);
|
|
1709
|
+
}
|
|
1710
|
+
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
1711
|
+
}
|
|
1712
|
+
var joiStringArrayType = (type) => (value, helpers) => {
|
|
1713
|
+
if (value === void 0 || typeof value === "function") {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
const arr = value.split(/,\s*/).map((el) => {
|
|
1717
|
+
if (type === "number") {
|
|
1718
|
+
const v = parseInt(el, 10);
|
|
1719
|
+
if (isNaN(v)) {
|
|
1720
|
+
throw new ParamError(`array element "${el}" should be numeric`);
|
|
1721
|
+
}
|
|
1722
|
+
return v;
|
|
1723
|
+
} else if (type === "boolean") {
|
|
1724
|
+
const v = el.match(/true|t|yes|1/i) ? true : el.match(/false|f|no|0/i) ? false : null;
|
|
1725
|
+
if (v === null) {
|
|
1726
|
+
throw new ParamError(`array element "${el}" should be boolean`);
|
|
1727
|
+
}
|
|
1728
|
+
return v;
|
|
1729
|
+
} else if (type === "string") {
|
|
1730
|
+
return el;
|
|
1731
|
+
} else {
|
|
1732
|
+
throw new ParamError(`unknown type "${type}" for array elements`);
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
return arr;
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
// src/params/index.ts
|
|
1739
|
+
var Params = class _Params {
|
|
1740
|
+
context;
|
|
1741
|
+
// Partial context during initialization
|
|
1742
|
+
params = {};
|
|
1743
|
+
paramSources = {};
|
|
1744
|
+
definitions = {};
|
|
1745
|
+
args;
|
|
1746
|
+
paramSetters = [];
|
|
1747
|
+
paramGetters = [];
|
|
1748
|
+
trackedParams = [];
|
|
1749
|
+
_currentModule = "script";
|
|
1750
|
+
/** Resolved early in constructor so cleanup does not read params lazily */
|
|
1751
|
+
_showUsedParams = false;
|
|
1752
|
+
constructor(context, options = {}) {
|
|
1753
|
+
this.context = context;
|
|
1754
|
+
this.args = context.args;
|
|
1755
|
+
if (Object.keys(options).length > 0) {
|
|
1756
|
+
this.configure(options);
|
|
1757
|
+
}
|
|
1758
|
+
this._showUsedParams = this.get("showUsedParams", "boolean default false");
|
|
1759
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1760
|
+
context.registerCleanup((ctx) => {
|
|
1761
|
+
if (!ctx.params.getShowUsedParams()) return;
|
|
1762
|
+
const byModule = ctx.params.getFiguredByModule();
|
|
1763
|
+
const modules = Object.keys(byModule).sort();
|
|
1764
|
+
if (modules.length === 0) return;
|
|
1765
|
+
const logger = ctx.logger;
|
|
1766
|
+
logger.debug("[Params]: list of used params:");
|
|
1767
|
+
if (typeof logger.highlight !== "function") {
|
|
1768
|
+
for (const mod of modules) {
|
|
1769
|
+
logger.debug(` [${mod}]`);
|
|
1770
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1771
|
+
logger.debug(` ${key}: ${JSON.stringify(entry.value)} (${entry.source})`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
for (const mod of modules) {
|
|
1777
|
+
logger.debug(` [${mod}]`);
|
|
1778
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1779
|
+
const valueStr = JSON.stringify(entry.value);
|
|
1780
|
+
const display = entry.source === "default" ? valueStr : logger.highlight(valueStr);
|
|
1781
|
+
logger.debug(` ${key}: ${display} (${entry.source})`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
/** Whether --showUsedParams was requested (resolved in constructor). */
|
|
1788
|
+
getShowUsedParams() {
|
|
1789
|
+
return this._showUsedParams;
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Configure parameters
|
|
1793
|
+
* Only parameters present in options are updated
|
|
1794
|
+
*/
|
|
1795
|
+
configure(options) {
|
|
1796
|
+
for (const [k, v] of Object.entries(options)) {
|
|
1797
|
+
this.params[k] = v;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Initialize Params from context and CLI parameters
|
|
1802
|
+
* Note: Params is special - it's initialized early with partial context
|
|
1803
|
+
*/
|
|
1804
|
+
static init(context, options) {
|
|
1805
|
+
return new _Params(context, options || {});
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Track a parameter request for --stopAfter=init and --showUsedParams
|
|
1809
|
+
*/
|
|
1810
|
+
trackParam(key, definition, value, source, moduleName) {
|
|
1811
|
+
this.trackedParams.push({
|
|
1812
|
+
key,
|
|
1813
|
+
definition,
|
|
1814
|
+
value,
|
|
1815
|
+
source,
|
|
1816
|
+
module: moduleName ?? this._currentModule
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Get all tracked parameters (for --stopAfter=init)
|
|
1821
|
+
*/
|
|
1822
|
+
getTrackedParams() {
|
|
1823
|
+
return [...this.trackedParams];
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Get all figured parameters as a record (flat, last occurrence per key)
|
|
1827
|
+
* Returns all parameters that were collected during initialization,
|
|
1828
|
+
* whether from CLI args, options, or defaults
|
|
1829
|
+
*/
|
|
1830
|
+
getAllFigured() {
|
|
1831
|
+
const result = {};
|
|
1832
|
+
for (const param of this.trackedParams) {
|
|
1833
|
+
result[param.key] = {
|
|
1834
|
+
value: param.value,
|
|
1835
|
+
source: param.source
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
return result;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Get figured parameters grouped by module name.
|
|
1842
|
+
* Same param can appear in multiple modules (e.g. source, resource).
|
|
1843
|
+
*/
|
|
1844
|
+
getFiguredByModule() {
|
|
1845
|
+
const byModule = {};
|
|
1846
|
+
for (const param of this.trackedParams) {
|
|
1847
|
+
const mod = param.module;
|
|
1848
|
+
if (!byModule[mod]) byModule[mod] = {};
|
|
1849
|
+
byModule[mod][param.key] = { value: param.value, source: param.source };
|
|
1850
|
+
}
|
|
1851
|
+
return byModule;
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Clear tracked parameters
|
|
1855
|
+
*/
|
|
1856
|
+
clearTrackedParams() {
|
|
1857
|
+
this.trackedParams = [];
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Assign a parameter definition
|
|
1861
|
+
*/
|
|
1862
|
+
assignDefinition(key, definition) {
|
|
1863
|
+
if (this.definitions[key] && !definition) {
|
|
1864
|
+
return this.definitions[key];
|
|
1865
|
+
}
|
|
1866
|
+
let type;
|
|
1867
|
+
if (!definition) {
|
|
1868
|
+
type = Joi.string();
|
|
1869
|
+
} else if (Joi.isSchema(definition)) {
|
|
1870
|
+
type = definition;
|
|
1871
|
+
} else if (Joi.isSchema(definition.type)) {
|
|
1872
|
+
type = definition.type;
|
|
1873
|
+
} else if (typeof definition === "string") {
|
|
1874
|
+
type = this.toJoi(definition);
|
|
1875
|
+
} else if (typeof definition.type === "string") {
|
|
1876
|
+
type = this.toJoi(definition.type);
|
|
1877
|
+
} else if (!definition.type) {
|
|
1878
|
+
type = Joi.string();
|
|
1879
|
+
} else {
|
|
1880
|
+
type = Joi.string();
|
|
1881
|
+
}
|
|
1882
|
+
if (!this.definitions[key]) {
|
|
1883
|
+
this.definitions[key] = {};
|
|
1884
|
+
}
|
|
1885
|
+
this.definitions[key].type = type;
|
|
1886
|
+
if (definition && definition.values) {
|
|
1887
|
+
if (Array.isArray(definition.values)) {
|
|
1888
|
+
this.definitions[key].values = definition.values;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return this.definitions[key];
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Convert string definition to Joi schema
|
|
1895
|
+
*/
|
|
1896
|
+
toJoi(str) {
|
|
1897
|
+
let type;
|
|
1898
|
+
if (str.match(/^string|^text/i)) {
|
|
1899
|
+
type = Joi.string();
|
|
1900
|
+
} else if (str.match(/^number|^integer|^int/i)) {
|
|
1901
|
+
type = Joi.number();
|
|
1902
|
+
} else if (str.match(/^boolean|^bool/i)) {
|
|
1903
|
+
type = Joi.boolean();
|
|
1904
|
+
} else if (str.match(/^date/i)) {
|
|
1905
|
+
type = Joi.custom(joiEdateType);
|
|
1906
|
+
} else if (str.match(/^duration/i)) {
|
|
1907
|
+
type = Joi.string().isoDuration();
|
|
1908
|
+
} else if (str.match(/^array/i)) {
|
|
1909
|
+
let elementTypes = "string";
|
|
1910
|
+
const tmp = str.match(/\((.*)\)/);
|
|
1911
|
+
if (tmp && tmp[1].match(/string/i)) {
|
|
1912
|
+
elementTypes = "string";
|
|
1913
|
+
} else if (tmp && tmp[1].match(/number|integer|int/i)) {
|
|
1914
|
+
elementTypes = "number";
|
|
1915
|
+
} else if (tmp && tmp[1].match(/boolean|bool/i)) {
|
|
1916
|
+
elementTypes = "boolean";
|
|
1917
|
+
}
|
|
1918
|
+
type = Joi.custom(joiStringArrayType(elementTypes));
|
|
1919
|
+
} else {
|
|
1920
|
+
type = Joi.string();
|
|
1921
|
+
}
|
|
1922
|
+
const regexForDefault = /\bdefault\s+([^\s]+)/;
|
|
1923
|
+
const matchForDefault = str.match(regexForDefault);
|
|
1924
|
+
if (matchForDefault) {
|
|
1925
|
+
const defValObj = type.validate(matchForDefault[1]);
|
|
1926
|
+
if (defValObj.error) {
|
|
1927
|
+
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1928
|
+
}
|
|
1929
|
+
type = type.default(defValObj.value);
|
|
1930
|
+
} else if (str.match(/required/)) {
|
|
1931
|
+
type = type.required();
|
|
1932
|
+
} else {
|
|
1933
|
+
type = type.optional();
|
|
1934
|
+
}
|
|
1935
|
+
return type;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Validate a value against a definition
|
|
1939
|
+
*/
|
|
1940
|
+
validate(key, val, def) {
|
|
1941
|
+
const normalizedVal = val === null ? void 0 : val;
|
|
1942
|
+
const { value, error } = def.type.validate(normalizedVal, {
|
|
1943
|
+
context: { params: this.params },
|
|
1944
|
+
abortEarly: false,
|
|
1945
|
+
allowUnknown: false
|
|
1946
|
+
});
|
|
1947
|
+
if (error) {
|
|
1948
|
+
const errs = error.details.map((el) => el.message).join(", ");
|
|
1949
|
+
throw new ParamError(`"${key}" validation error: ${errs}`);
|
|
1950
|
+
}
|
|
1951
|
+
return value;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Get a parameter value with validation
|
|
1955
|
+
*/
|
|
1956
|
+
get(key, definition) {
|
|
1957
|
+
const def = this.assignDefinition(key, definition);
|
|
1958
|
+
let valFromGetters = void 0;
|
|
1959
|
+
if (def.volatile || true) {
|
|
1960
|
+
valFromGetters = this.runAllRegisteredGetters(key);
|
|
1961
|
+
}
|
|
1962
|
+
const valFromArgs = this.args.get(key);
|
|
1963
|
+
const valFromParams = this.params[key];
|
|
1964
|
+
let source = "default";
|
|
1965
|
+
let value;
|
|
1966
|
+
if (valFromGetters !== void 0 && valFromGetters !== null) {
|
|
1967
|
+
value = this.validate(key, valFromGetters, def);
|
|
1968
|
+
source = "options";
|
|
1969
|
+
} else if (valFromArgs !== void 0 && valFromArgs !== null) {
|
|
1970
|
+
value = this.validate(key, valFromArgs, def);
|
|
1971
|
+
const argsSource = this.args.getSource?.(key);
|
|
1972
|
+
if (argsSource === "overrides") source = "options";
|
|
1973
|
+
else if (argsSource === "cli" || argsSource === "env" || argsSource === "config") source = argsSource;
|
|
1974
|
+
else if (argsSource === "default") source = "default";
|
|
1975
|
+
else source = "cli";
|
|
1976
|
+
} else if (valFromParams !== void 0 && valFromParams !== null) {
|
|
1977
|
+
value = this.validate(key, valFromParams, def);
|
|
1978
|
+
source = this.paramSources[key] ?? "options";
|
|
1979
|
+
} else {
|
|
1980
|
+
value = this.validate(key, void 0, def);
|
|
1981
|
+
source = "default";
|
|
1982
|
+
}
|
|
1983
|
+
this.paramSources[key] = source;
|
|
1984
|
+
this.trackParam(key, definition || "string", value, source);
|
|
1985
|
+
if (value !== void 0 && def.values && !def.values.includes(value)) {
|
|
1986
|
+
throw new ParamError(`key ${key} should be one of ${def.values}`);
|
|
1987
|
+
}
|
|
1988
|
+
return value;
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Set a parameter value with validation
|
|
1992
|
+
*/
|
|
1993
|
+
set(key, val, definition) {
|
|
1994
|
+
if (val && val.type && val.value) {
|
|
1995
|
+
definition = val;
|
|
1996
|
+
val = val.value;
|
|
1997
|
+
}
|
|
1998
|
+
const def = this.assignDefinition(key, definition);
|
|
1999
|
+
if (!this.runAllRegisteredSetters(key, val)) {
|
|
2000
|
+
this.params[key] = val;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Get all parameters from definitions (main script).
|
|
2005
|
+
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
2006
|
+
*/
|
|
2007
|
+
getAll(defs2) {
|
|
2008
|
+
return this.getAllForModule("script", defs2);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Get all parameters from definitions for a given module name.
|
|
2012
|
+
* Figured params are grouped by module when using --showUsedParams.
|
|
2013
|
+
* Processes parameters left-to-right to support cross-parameter references.
|
|
2014
|
+
* If moduleName is omitted, it is inferred from the caller's file path (directory name under src/).
|
|
2015
|
+
*/
|
|
2016
|
+
getAllForModule(moduleNameOrDefs, defs2) {
|
|
2017
|
+
let moduleName;
|
|
2018
|
+
let definitions;
|
|
2019
|
+
if (defs2 !== void 0) {
|
|
2020
|
+
moduleName = moduleNameOrDefs;
|
|
2021
|
+
definitions = defs2;
|
|
2022
|
+
} else {
|
|
2023
|
+
definitions = moduleNameOrDefs;
|
|
2024
|
+
moduleName = this._inferModuleNameFromStack();
|
|
2025
|
+
}
|
|
2026
|
+
const prev = this._currentModule;
|
|
2027
|
+
this._currentModule = moduleName;
|
|
2028
|
+
try {
|
|
2029
|
+
const res = {};
|
|
2030
|
+
for (const [k, def] of Object.entries(definitions)) {
|
|
2031
|
+
const value = this.get(k, def);
|
|
2032
|
+
res[k] = value;
|
|
2033
|
+
if (value !== void 0) {
|
|
2034
|
+
this.params[k] = value;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return res;
|
|
2038
|
+
} finally {
|
|
2039
|
+
this._currentModule = prev;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2044
|
+
*/
|
|
2045
|
+
_inferModuleNameFromStack() {
|
|
2046
|
+
const stack = new Error().stack;
|
|
2047
|
+
if (!stack) return "script";
|
|
2048
|
+
const lines = stack.split("\n");
|
|
2049
|
+
const paramsIndexPath = "params" + (typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/") + "index.";
|
|
2050
|
+
for (const line of lines) {
|
|
2051
|
+
const parenMatch = line.match(/\(([^)]+)\)/);
|
|
2052
|
+
if (!parenMatch) continue;
|
|
2053
|
+
const parts = parenMatch[1].split(":");
|
|
2054
|
+
if (parts.length < 3) continue;
|
|
2055
|
+
const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2056
|
+
if (!path5 || path5.includes(paramsIndexPath)) continue;
|
|
2057
|
+
const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2058
|
+
if (srcMatch) return srcMatch[1];
|
|
2059
|
+
}
|
|
2060
|
+
return "script";
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Run all registered getters for a key
|
|
2064
|
+
*/
|
|
2065
|
+
runAllRegisteredGetters(key) {
|
|
2066
|
+
let val = void 0;
|
|
2067
|
+
for (const getter of this.paramGetters) {
|
|
2068
|
+
val = getter(key, this.definitions[key]);
|
|
2069
|
+
if (val !== void 0 && val !== null) {
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return val;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Run all registered setters for a key
|
|
2077
|
+
*/
|
|
2078
|
+
runAllRegisteredSetters(key, value) {
|
|
2079
|
+
let setterUsed = false;
|
|
2080
|
+
for (const setter of this.paramSetters) {
|
|
2081
|
+
setterUsed = setter(key, value);
|
|
2082
|
+
if (setterUsed) {
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return setterUsed;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Register a parameter getter
|
|
2090
|
+
*/
|
|
2091
|
+
registerParamGetter(fn) {
|
|
2092
|
+
this.paramGetters.push(fn);
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Register a parameter setter
|
|
2096
|
+
*/
|
|
2097
|
+
registerParamSetter(fn) {
|
|
2098
|
+
this.paramSetters.push(fn);
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// src/logger/index.ts
|
|
2103
|
+
import chalk from "chalk";
|
|
2104
|
+
import util from "util";
|
|
2105
|
+
|
|
2106
|
+
// src/logger/transports.ts
|
|
2107
|
+
var ConsoleTransport = class {
|
|
2108
|
+
write(payload) {
|
|
2109
|
+
console.info(payload);
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
var ParentProcessTransport = class {
|
|
2113
|
+
write(payload) {
|
|
2114
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
|
2115
|
+
console.info(payload);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (typeof process.send === "function" && process.connected === true) {
|
|
2119
|
+
process.send(payload);
|
|
2120
|
+
} else {
|
|
2121
|
+
console.info(payload);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
// src/logger/index.ts
|
|
2127
|
+
var ALL_LEVELS = [
|
|
2128
|
+
"silly",
|
|
2129
|
+
"debug",
|
|
2130
|
+
"logic",
|
|
2131
|
+
"info",
|
|
2132
|
+
"notice",
|
|
2133
|
+
"warn",
|
|
2134
|
+
"error",
|
|
2135
|
+
"results",
|
|
2136
|
+
"request",
|
|
2137
|
+
"response",
|
|
2138
|
+
"progress"
|
|
2139
|
+
];
|
|
2140
|
+
var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
|
|
2141
|
+
var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
|
|
2142
|
+
var LEVEL_COLORS = {
|
|
2143
|
+
error: chalk.red.bold,
|
|
2144
|
+
warn: chalk.rgb(255, 165, 0),
|
|
2145
|
+
notice: chalk.cyan,
|
|
2146
|
+
info: chalk.white.bold,
|
|
2147
|
+
logic: chalk.gray,
|
|
2148
|
+
debug: chalk.gray,
|
|
2149
|
+
silly: chalk.gray,
|
|
2150
|
+
request: chalk.green,
|
|
2151
|
+
response: chalk.yellow,
|
|
2152
|
+
progress: chalk.green,
|
|
2153
|
+
results: chalk.magenta
|
|
2154
|
+
};
|
|
2155
|
+
var Logger = class _Logger {
|
|
2156
|
+
context;
|
|
2157
|
+
// Partial context during initialization
|
|
2158
|
+
options;
|
|
2159
|
+
transport;
|
|
2160
|
+
startTimes = {};
|
|
2161
|
+
lastProgressTimes = {};
|
|
2162
|
+
constructor(context, options = {}) {
|
|
2163
|
+
this.context = context;
|
|
2164
|
+
this.options = this.getDefaultOptions();
|
|
2165
|
+
if (options) {
|
|
2166
|
+
this.configure(options);
|
|
2167
|
+
}
|
|
2168
|
+
this.updateTransport();
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* Configure logger options. Accepts both LoggerOptions shape and flat param names (levels string, progressWithTimes, progressThrottleMs).
|
|
2172
|
+
*/
|
|
2173
|
+
configure(options) {
|
|
2174
|
+
if (options.mode !== void 0) {
|
|
2175
|
+
this.options.mode = this.isValidMode(options.mode) ? options.mode : "text";
|
|
2176
|
+
}
|
|
2177
|
+
if (options.route !== void 0) {
|
|
2178
|
+
this.options.route = options.route;
|
|
2179
|
+
this.updateTransport();
|
|
2180
|
+
}
|
|
2181
|
+
if (options.prefix !== void 0) this.options.prefix = options.prefix;
|
|
2182
|
+
if (options.silent !== void 0) this.options.silent = options.silent;
|
|
2183
|
+
if (options.showLevel !== void 0) this.options.showLevel = options.showLevel;
|
|
2184
|
+
if (options.timestamp !== void 0) this.options.timestamp = options.timestamp;
|
|
2185
|
+
if (options.levels !== void 0) {
|
|
2186
|
+
const levels = typeof options.levels === "string" ? options.levels.split(",") : options.levels;
|
|
2187
|
+
this.options.levels = this.normalizeLevels(levels);
|
|
2188
|
+
}
|
|
2189
|
+
if (options.progress !== void 0) {
|
|
2190
|
+
if (options.progress.withTimes !== void 0) this.options.progressTimes = options.progress.withTimes;
|
|
2191
|
+
if (options.progress.throttleMs !== void 0) this.options.progressThrottle = options.progress.throttleMs;
|
|
2192
|
+
}
|
|
2193
|
+
const flat = options;
|
|
2194
|
+
if (flat.progressWithTimes !== void 0) this.options.progressTimes = flat.progressWithTimes;
|
|
2195
|
+
if (flat.progressThrottleMs !== void 0) this.options.progressThrottle = flat.progressThrottleMs;
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2199
|
+
*/
|
|
2200
|
+
static init(context, options) {
|
|
2201
|
+
const paramDefs = {
|
|
2202
|
+
mode: "string default text",
|
|
2203
|
+
route: "string default console",
|
|
2204
|
+
prefix: "string",
|
|
2205
|
+
silent: "boolean default false",
|
|
2206
|
+
showLevel: "boolean default true",
|
|
2207
|
+
timestamp: "boolean default false",
|
|
2208
|
+
levels: "string",
|
|
2209
|
+
progressWithTimes: "boolean default false",
|
|
2210
|
+
progressThrottleMs: "number"
|
|
2211
|
+
};
|
|
2212
|
+
const discovered = context.params.getAllForModule(paramDefs);
|
|
2213
|
+
const config2 = { ...discovered, ...options };
|
|
2214
|
+
const logger = new _Logger(context, config2);
|
|
2215
|
+
context.logger = logger;
|
|
2216
|
+
return logger;
|
|
2217
|
+
}
|
|
2218
|
+
getDefaultOptions() {
|
|
2219
|
+
return {
|
|
2220
|
+
mode: "text",
|
|
2221
|
+
route: this.shouldUseIpcRoute() ? "ipc" : "console",
|
|
2222
|
+
prefix: void 0,
|
|
2223
|
+
silent: false,
|
|
2224
|
+
showLevel: false,
|
|
2225
|
+
timestamp: false,
|
|
2226
|
+
levels: DEFAULT_LEVELS,
|
|
2227
|
+
progressTimes: false,
|
|
2228
|
+
progressThrottle: void 0
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
updateTransport() {
|
|
2232
|
+
this.transport = this.options.route === "ipc" ? new ParentProcessTransport() : new ConsoleTransport();
|
|
2233
|
+
}
|
|
2234
|
+
setMode(mode) {
|
|
2235
|
+
if (!this.isValidMode(mode)) {
|
|
2236
|
+
throw new Error(`Unsupported logger mode: ${mode}`);
|
|
2237
|
+
}
|
|
2238
|
+
this.options.mode = mode;
|
|
2239
|
+
}
|
|
2240
|
+
/** Returns a styled string (bright white) for highlighting; keeps chalk inside logger. */
|
|
2241
|
+
highlight(text) {
|
|
2242
|
+
return chalk.whiteBright(text);
|
|
2243
|
+
}
|
|
2244
|
+
debug(message, ...chunks) {
|
|
2245
|
+
this.out({ level: "debug", message, chunks });
|
|
2246
|
+
}
|
|
2247
|
+
info(message, ...chunks) {
|
|
2248
|
+
this.out({ level: "info", message, chunks });
|
|
2249
|
+
}
|
|
2250
|
+
notice(message, ...chunks) {
|
|
2251
|
+
this.out({ level: "notice", message, chunks });
|
|
2252
|
+
}
|
|
2253
|
+
warn(message, ...chunks) {
|
|
2254
|
+
this.out({ level: "warn", message, chunks });
|
|
2255
|
+
}
|
|
2256
|
+
error(message, ...chunks) {
|
|
2257
|
+
this.out({ level: "error", message, chunks });
|
|
2258
|
+
}
|
|
2259
|
+
logic(message, ...chunks) {
|
|
2260
|
+
this.out({ level: "logic", message, chunks });
|
|
2261
|
+
}
|
|
2262
|
+
silly(message, ...chunks) {
|
|
2263
|
+
this.out({ level: "silly", message, chunks });
|
|
2264
|
+
}
|
|
2265
|
+
results(results) {
|
|
2266
|
+
this.out({ level: "results", message: "results", results });
|
|
2267
|
+
}
|
|
2268
|
+
request(operation, ...chunks) {
|
|
2269
|
+
const message = this.inspectChunks([operation, ...chunks]);
|
|
2270
|
+
this.out({ level: "request", message });
|
|
2271
|
+
}
|
|
2272
|
+
response(operation, ...chunks) {
|
|
2273
|
+
const message = this.inspectChunks([operation, ...chunks]);
|
|
2274
|
+
this.out({ level: "response", message });
|
|
2275
|
+
}
|
|
2276
|
+
progress(message, opts) {
|
|
2277
|
+
const { prefix, count, total } = opts;
|
|
2278
|
+
const paddedTotal = String(total).length;
|
|
2279
|
+
const paddedCount = String(count).padStart(paddedTotal, " ");
|
|
2280
|
+
const payload = {
|
|
2281
|
+
level: "progress",
|
|
2282
|
+
message,
|
|
2283
|
+
count: paddedCount,
|
|
2284
|
+
total,
|
|
2285
|
+
prefix
|
|
2286
|
+
};
|
|
2287
|
+
if (!this.startTimes[prefix ?? ""]) {
|
|
2288
|
+
this.startTimes[prefix ?? ""] = Date.now();
|
|
2289
|
+
}
|
|
2290
|
+
if (this.options.progressTimes) {
|
|
2291
|
+
const elapsedSeconds = (Date.now() - this.startTimes[prefix ?? ""]) / 1e3;
|
|
2292
|
+
let remaining = -1;
|
|
2293
|
+
if (count > 1) {
|
|
2294
|
+
const rate = elapsedSeconds / (count - 1);
|
|
2295
|
+
remaining = (total - count) * rate;
|
|
2296
|
+
}
|
|
2297
|
+
payload.elapsed = this.round(elapsedSeconds, 2);
|
|
2298
|
+
payload.remaining = remaining >= 0 ? this.round(remaining, 2) : remaining;
|
|
2299
|
+
}
|
|
2300
|
+
if (count >= total) {
|
|
2301
|
+
delete this.startTimes[prefix ?? ""];
|
|
2302
|
+
delete this.lastProgressTimes[prefix ?? ""];
|
|
2303
|
+
}
|
|
2304
|
+
if (this.shouldOutputProgress(prefix ?? "", count, total)) {
|
|
2305
|
+
this.out(payload);
|
|
2306
|
+
if (this.options.progressThrottle && prefix) {
|
|
2307
|
+
this.lastProgressTimes[prefix] = Date.now();
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
shouldOutputProgress(prefix, count, total) {
|
|
2312
|
+
if (!this.options.progressThrottle) {
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
if (count === 1 || count === total || !prefix) {
|
|
2316
|
+
return true;
|
|
2317
|
+
}
|
|
2318
|
+
const lastTime = this.lastProgressTimes[prefix];
|
|
2319
|
+
if (!lastTime) {
|
|
2320
|
+
return true;
|
|
2321
|
+
}
|
|
2322
|
+
return Date.now() - lastTime >= this.options.progressThrottle;
|
|
2323
|
+
}
|
|
2324
|
+
out(struct) {
|
|
2325
|
+
if (this.options.silent) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
if (!this.options.levels.includes(struct.level)) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (this.options.prefix && !struct.prefix) {
|
|
2332
|
+
struct.prefix = this.options.prefix;
|
|
2333
|
+
}
|
|
2334
|
+
const output = this.options.mode === "json" ? struct : this.formatLog(struct);
|
|
2335
|
+
this.transport.write(output);
|
|
2336
|
+
}
|
|
2337
|
+
formatLog(struct) {
|
|
2338
|
+
const parts = [];
|
|
2339
|
+
const now = /* @__PURE__ */ new Date();
|
|
2340
|
+
if (this.options.timestamp) {
|
|
2341
|
+
parts.push(now.toISOString());
|
|
2342
|
+
}
|
|
2343
|
+
if (this.options.showLevel) {
|
|
2344
|
+
parts.push(struct.level.toUpperCase().padEnd(MAX_LEVEL_LENGTH));
|
|
2345
|
+
}
|
|
2346
|
+
if (struct.level === "progress") {
|
|
2347
|
+
if (struct.prefix) {
|
|
2348
|
+
parts.push(LEVEL_COLORS[struct.level].bold(struct.prefix));
|
|
2349
|
+
}
|
|
2350
|
+
if (struct.count !== void 0 && struct.total !== void 0) {
|
|
2351
|
+
parts.push(LEVEL_COLORS[struct.level](`${struct.count}/${struct.total}`));
|
|
2352
|
+
}
|
|
2353
|
+
} else if (struct.prefix) {
|
|
2354
|
+
parts.push(chalk.cyan(`[${struct.prefix}]`));
|
|
2355
|
+
}
|
|
2356
|
+
if (struct.message) {
|
|
2357
|
+
const formatter = LEVEL_COLORS[struct.level] ?? chalk.white;
|
|
2358
|
+
parts.push(formatter.bold(struct.message));
|
|
2359
|
+
}
|
|
2360
|
+
if (struct.level === "progress") {
|
|
2361
|
+
if (struct.elapsed !== void 0 && struct.remaining !== void 0) {
|
|
2362
|
+
const formatter = LEVEL_COLORS[struct.level];
|
|
2363
|
+
parts.push(formatter(`${struct.elapsed}/${struct.remaining}`));
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (struct.chunks && struct.chunks.length) {
|
|
2367
|
+
parts.push(this.inspectChunks(struct.chunks));
|
|
2368
|
+
}
|
|
2369
|
+
if (struct.results) {
|
|
2370
|
+
const formatter = LEVEL_COLORS[struct.level] ?? chalk.white;
|
|
2371
|
+
parts.push(formatter(JSON.stringify(struct.results, null, 4)));
|
|
2372
|
+
}
|
|
2373
|
+
return parts.join(" ");
|
|
2374
|
+
}
|
|
2375
|
+
inspectChunks(chunks) {
|
|
2376
|
+
return chunks.map((chunk) => util.inspect(chunk, { colors: true, depth: null })).join(" ");
|
|
2377
|
+
}
|
|
2378
|
+
shouldUseIpcRoute() {
|
|
2379
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
return typeof process.send === "function" && process.connected === true;
|
|
2383
|
+
}
|
|
2384
|
+
normalizeLevels(levels) {
|
|
2385
|
+
if (!levels || !levels.length) {
|
|
2386
|
+
return DEFAULT_LEVELS;
|
|
2387
|
+
}
|
|
2388
|
+
const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
|
|
2389
|
+
const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
|
|
2390
|
+
const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
|
|
2391
|
+
const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
|
|
2392
|
+
const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
|
|
2393
|
+
if (unknown.length) {
|
|
2394
|
+
console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
|
|
2395
|
+
}
|
|
2396
|
+
const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
|
|
2397
|
+
return base.filter((level) => !excludes.includes(level));
|
|
2398
|
+
}
|
|
2399
|
+
isValidMode(mode) {
|
|
2400
|
+
return mode === void 0 || mode === null || mode === "text" || mode === "json";
|
|
2401
|
+
}
|
|
2402
|
+
round(value, places) {
|
|
2403
|
+
const factor = Math.pow(10, places);
|
|
2404
|
+
return Math.round(value * factor) / factor;
|
|
2405
|
+
}
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
// src/init/index.ts
|
|
2409
|
+
import { EventEmitter } from "events";
|
|
2410
|
+
function extractComponentOptions(opts, componentName) {
|
|
2411
|
+
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2412
|
+
const componentOptions = {};
|
|
2413
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
2414
|
+
if (!reservedKeys.includes(key)) {
|
|
2415
|
+
componentOptions[key] = value;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return componentOptions;
|
|
2419
|
+
}
|
|
2420
|
+
function setup(opts = {}) {
|
|
2421
|
+
const partialContext = {
|
|
2422
|
+
emitter: new EventEmitter(),
|
|
2423
|
+
isStop: () => false,
|
|
2424
|
+
cleanupFunctions: [],
|
|
2425
|
+
registerCleanup: (fn) => {
|
|
2426
|
+
partialContext.cleanupFunctions.push(fn);
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
const args = Args.init(partialContext, {
|
|
2430
|
+
overrides: opts.overrides || {},
|
|
2431
|
+
defaults: opts.defaults || {}
|
|
2432
|
+
});
|
|
2433
|
+
partialContext.args = args;
|
|
2434
|
+
const params = Params.init(partialContext, opts.overrides || {});
|
|
2435
|
+
partialContext.params = params;
|
|
2436
|
+
const loggerOptions = extractComponentOptions(opts, "logger");
|
|
2437
|
+
const logger = Logger.init(partialContext, loggerOptions);
|
|
2438
|
+
partialContext.logger = logger;
|
|
2439
|
+
const context = {
|
|
2440
|
+
args,
|
|
2441
|
+
params,
|
|
2442
|
+
logger,
|
|
2443
|
+
emitter: partialContext.emitter,
|
|
2444
|
+
isStop: partialContext.isStop,
|
|
2445
|
+
cleanupFunctions: partialContext.cleanupFunctions,
|
|
2446
|
+
registerCleanup: partialContext.registerCleanup
|
|
2447
|
+
};
|
|
2448
|
+
logger.debug("[setup] completed successfully");
|
|
2449
|
+
return context;
|
|
2450
|
+
}
|
|
2451
|
+
async function setupModules(context, opts = {}) {
|
|
2452
|
+
if (opts.modules && opts.modules.length > 0) {
|
|
2453
|
+
context.logger.debug(`[setupModules] modules specified: ${opts.modules.join(", ")} (not yet implemented)`);
|
|
2454
|
+
}
|
|
2455
|
+
context.logger.debug("[setupModules] completed successfully");
|
|
2456
|
+
return context;
|
|
2457
|
+
}
|
|
2458
|
+
function printAllParameters(context) {
|
|
2459
|
+
const trackedParams = context.params.getTrackedParams();
|
|
2460
|
+
console.log("\n=== All Figured Parameters ===");
|
|
2461
|
+
console.log("\nComponent: Logger");
|
|
2462
|
+
const loggerParams = trackedParams.filter(
|
|
2463
|
+
(p) => ["mode", "route", "prefix", "silent", "showLevel", "timestamp", "levels"].includes(p.key)
|
|
2464
|
+
);
|
|
2465
|
+
if (loggerParams.length > 0) {
|
|
2466
|
+
loggerParams.forEach((p) => {
|
|
2467
|
+
console.log(` ${p.key}: ${JSON.stringify(p.value)} (from ${p.source})`);
|
|
2468
|
+
});
|
|
2469
|
+
} else {
|
|
2470
|
+
console.log(" (no parameters requested)");
|
|
2471
|
+
}
|
|
2472
|
+
console.log("\n=== End Parameters ===\n");
|
|
2473
|
+
}
|
|
2474
|
+
async function init(flow2, opts = {}) {
|
|
2475
|
+
let stop = false;
|
|
2476
|
+
let context = null;
|
|
2477
|
+
try {
|
|
2478
|
+
try {
|
|
2479
|
+
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
2480
|
+
if (screenModule && typeof screenModule.load === "function") {
|
|
2481
|
+
await screenModule.load();
|
|
2482
|
+
}
|
|
2483
|
+
} catch {
|
|
2484
|
+
if (typeof __require !== "undefined") {
|
|
2485
|
+
try {
|
|
2486
|
+
} catch {
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
context = setup(opts);
|
|
2491
|
+
context.isStop = () => stop;
|
|
2492
|
+
context = await setupModules(context, opts);
|
|
2493
|
+
const stopAfter = context.args.get("stopAfter");
|
|
2494
|
+
const stopAllowance = context.params.get("stopAllowance", "number default 5");
|
|
2495
|
+
if (stopAfter === "init") {
|
|
2496
|
+
printAllParameters(context);
|
|
2497
|
+
process.exit(0);
|
|
2498
|
+
}
|
|
2499
|
+
process.on("SIGINT", async () => {
|
|
2500
|
+
if (stop) {
|
|
2501
|
+
context.logger.warn("[process] killed");
|
|
2502
|
+
process.exit(2);
|
|
2503
|
+
}
|
|
2504
|
+
stop = true;
|
|
2505
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2506
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2507
|
+
});
|
|
2508
|
+
await flow2(context);
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
const errorLocation = error instanceof Error && error.stack ? error.stack.split("\n")[1]?.trim() || "Unknown location" : "Unknown location";
|
|
2511
|
+
const logError = (msg, ...args) => {
|
|
2512
|
+
if (context?.logger) {
|
|
2513
|
+
context.logger.error(msg, ...args);
|
|
2514
|
+
} else {
|
|
2515
|
+
console.error(msg, ...args);
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
if (error instanceof ParamError) {
|
|
2519
|
+
logError(`[params]: ${error.message} (${errorLocation})`);
|
|
2520
|
+
process.exitCode = 3;
|
|
2521
|
+
} else if (error instanceof InitError) {
|
|
2522
|
+
logError(`[init]: ${error.message} (${errorLocation})`);
|
|
2523
|
+
process.exitCode = 4;
|
|
2524
|
+
} else {
|
|
2525
|
+
logError(`[other] error:`, error, errorLocation);
|
|
2526
|
+
process.exitCode = 5;
|
|
2527
|
+
}
|
|
2528
|
+
} finally {
|
|
2529
|
+
if (context) {
|
|
2530
|
+
for (const fn of context.cleanupFunctions.reverse()) {
|
|
2531
|
+
try {
|
|
2532
|
+
await fn(context);
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/db/index.ts
|
|
2542
|
+
import knex from "knex";
|
|
2543
|
+
var Db = class {
|
|
2544
|
+
knexInstance = null;
|
|
2545
|
+
config;
|
|
2546
|
+
logger;
|
|
2547
|
+
queriesLog = [];
|
|
2548
|
+
isConnected = false;
|
|
2549
|
+
/**
|
|
2550
|
+
* Constructor - accepts config object
|
|
2551
|
+
* Use dbInit() function to initialize with Context
|
|
2552
|
+
*/
|
|
2553
|
+
constructor(config2) {
|
|
2554
|
+
if (!config2.connectionString) {
|
|
2555
|
+
throw new ParamError("Db: connectionString is required");
|
|
2556
|
+
}
|
|
2557
|
+
this.config = {
|
|
2558
|
+
testConnection: true,
|
|
2559
|
+
profile: false,
|
|
2560
|
+
pool: { min: 2, max: 10 },
|
|
2561
|
+
acquireConnectionTimeout: 1e4,
|
|
2562
|
+
ssl: { rejectUnauthorized: false },
|
|
2563
|
+
logger: console,
|
|
2564
|
+
name: "default",
|
|
2565
|
+
...config2
|
|
2566
|
+
};
|
|
2567
|
+
this.logger = this.config.logger;
|
|
2568
|
+
const instance = this;
|
|
2569
|
+
const callableWrapper = function(...args) {
|
|
2570
|
+
throw new Error("This should never be called directly");
|
|
2571
|
+
};
|
|
2572
|
+
callableWrapper._instance = instance;
|
|
2573
|
+
return new Proxy(callableWrapper, {
|
|
2574
|
+
// Intercept function calls: db('table')
|
|
2575
|
+
apply: (target, thisArg, argumentsList) => {
|
|
2576
|
+
const inst = target._instance;
|
|
2577
|
+
if (!inst.knexInstance) {
|
|
2578
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2579
|
+
}
|
|
2580
|
+
return inst.knexInstance(...argumentsList);
|
|
2581
|
+
},
|
|
2582
|
+
// Intercept property access: db.schema, db.raw, etc.
|
|
2583
|
+
get: (target, prop) => {
|
|
2584
|
+
if (prop === "_instance") {
|
|
2585
|
+
return target._instance;
|
|
2586
|
+
}
|
|
2587
|
+
const instance2 = target._instance;
|
|
2588
|
+
const ownMethods = [
|
|
2589
|
+
"connect",
|
|
2590
|
+
"disconnect",
|
|
2591
|
+
"testConnection",
|
|
2592
|
+
"tableExists",
|
|
2593
|
+
"getQueryLog",
|
|
2594
|
+
"getKnex",
|
|
2595
|
+
"isConnectedToDb",
|
|
2596
|
+
"getErrorMessage",
|
|
2597
|
+
"detectClient",
|
|
2598
|
+
"attachProfiler"
|
|
2599
|
+
];
|
|
2600
|
+
if (prop in instance2) {
|
|
2601
|
+
const value = instance2[prop];
|
|
2602
|
+
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2603
|
+
return value.bind(instance2);
|
|
2604
|
+
}
|
|
2605
|
+
if (typeof value !== "function") {
|
|
2606
|
+
return value;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (instance2.knexInstance) {
|
|
2610
|
+
const knexProp = instance2.knexInstance[prop];
|
|
2611
|
+
if (typeof knexProp === "function") {
|
|
2612
|
+
return knexProp.bind(instance2.knexInstance);
|
|
2613
|
+
}
|
|
2614
|
+
return knexProp;
|
|
2615
|
+
}
|
|
2616
|
+
if (prop in instance2) {
|
|
2617
|
+
const method = instance2[prop];
|
|
2618
|
+
if (typeof method === "function") {
|
|
2619
|
+
return method.bind(instance2);
|
|
2620
|
+
}
|
|
2621
|
+
return method;
|
|
2622
|
+
}
|
|
2623
|
+
return void 0;
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Detect database client type from connection string
|
|
2629
|
+
*/
|
|
2630
|
+
detectClient(connectionString) {
|
|
2631
|
+
if (connectionString.match(/^postgresql/)) {
|
|
2632
|
+
return "pg";
|
|
2633
|
+
}
|
|
2634
|
+
if (connectionString.match(/^mysql/)) {
|
|
2635
|
+
return "mysql2";
|
|
2636
|
+
}
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Connect to the database
|
|
2641
|
+
*/
|
|
2642
|
+
async connect() {
|
|
2643
|
+
if (this.isConnected && this.knexInstance) {
|
|
2644
|
+
this.logger.warn?.("[Db] Already connected");
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const client = this.detectClient(this.config.connectionString);
|
|
2648
|
+
if (!client) {
|
|
2649
|
+
throw new ParamError(
|
|
2650
|
+
`Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://`
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
try {
|
|
2654
|
+
const connectionConfig = {
|
|
2655
|
+
connectionString: this.config.connectionString,
|
|
2656
|
+
family: 4
|
|
2657
|
+
// Force IPv4 only (disable IPv6)
|
|
2658
|
+
};
|
|
2659
|
+
this.knexInstance = knex({
|
|
2660
|
+
client,
|
|
2661
|
+
connection: connectionConfig,
|
|
2662
|
+
pool: this.config.pool,
|
|
2663
|
+
acquireConnectionTimeout: this.config.acquireConnectionTimeout,
|
|
2664
|
+
...this.config.ssl && { ssl: this.config.ssl }
|
|
2665
|
+
});
|
|
2666
|
+
if (this.config.profile) {
|
|
2667
|
+
this.attachProfiler();
|
|
2668
|
+
}
|
|
2669
|
+
if (this.config.testConnection) {
|
|
2670
|
+
await this.testConnection();
|
|
2671
|
+
}
|
|
2672
|
+
this.isConnected = true;
|
|
2673
|
+
this.logger.debug?.(`[Db] Connected to database "${this.config.name || this.config.connectionString}"`);
|
|
2674
|
+
} catch (error) {
|
|
2675
|
+
if (error instanceof ParamError) {
|
|
2676
|
+
throw error;
|
|
2677
|
+
}
|
|
2678
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2679
|
+
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Disconnect from the database
|
|
2684
|
+
*/
|
|
2685
|
+
async disconnect() {
|
|
2686
|
+
if (!this.knexInstance) {
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
try {
|
|
2690
|
+
await this.knexInstance.destroy();
|
|
2691
|
+
this.knexInstance = null;
|
|
2692
|
+
this.isConnected = false;
|
|
2693
|
+
this.queriesLog = [];
|
|
2694
|
+
this.logger.debug?.(`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`);
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2697
|
+
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2698
|
+
throw error;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
/**
|
|
2702
|
+
* Extract error message from various error types
|
|
2703
|
+
*/
|
|
2704
|
+
getErrorMessage(error) {
|
|
2705
|
+
if (error instanceof AggregateError) {
|
|
2706
|
+
const errors = error.errors || [];
|
|
2707
|
+
if (errors.length > 0) {
|
|
2708
|
+
const firstError = errors[0];
|
|
2709
|
+
const firstErrorMsg = firstError instanceof Error ? firstError.message : String(firstError);
|
|
2710
|
+
const allSimilar = errors.every((e) => {
|
|
2711
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2712
|
+
const codeMatch = msg.match(/^(\w+)\s/);
|
|
2713
|
+
const firstCodeMatch = firstErrorMsg.match(/^(\w+)\s/);
|
|
2714
|
+
return codeMatch && firstCodeMatch && codeMatch[1] === firstCodeMatch[1];
|
|
2715
|
+
});
|
|
2716
|
+
if (allSimilar && errors.length > 1) {
|
|
2717
|
+
const addresses = errors.map((e) => {
|
|
2718
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2719
|
+
const addrMatch = msg.match(/([:\d.]+:\d+)/);
|
|
2720
|
+
return addrMatch ? addrMatch[1] : null;
|
|
2721
|
+
}).filter(Boolean);
|
|
2722
|
+
if (addresses.length > 0) {
|
|
2723
|
+
const codeMatch = firstErrorMsg.match(/^(\w+)\s/);
|
|
2724
|
+
const code = codeMatch ? codeMatch[1] : "Connection error";
|
|
2725
|
+
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
const uniqueMessages = [...new Set(errors.map((e) => {
|
|
2729
|
+
return e instanceof Error ? e.message : String(e);
|
|
2730
|
+
}))];
|
|
2731
|
+
if (uniqueMessages.length === 1) {
|
|
2732
|
+
return uniqueMessages[0];
|
|
2733
|
+
}
|
|
2734
|
+
return uniqueMessages.join("; ");
|
|
2735
|
+
}
|
|
2736
|
+
return error.message || "Multiple errors occurred";
|
|
2737
|
+
}
|
|
2738
|
+
if (error instanceof Error) {
|
|
2739
|
+
const errorWithCode = error;
|
|
2740
|
+
if (errorWithCode.code) {
|
|
2741
|
+
return `${errorWithCode.code}: ${error.message || String(error)}`;
|
|
2742
|
+
}
|
|
2743
|
+
return error.message || String(error);
|
|
2744
|
+
}
|
|
2745
|
+
if (typeof error === "string") {
|
|
2746
|
+
return error;
|
|
2747
|
+
}
|
|
2748
|
+
if (error?.message) {
|
|
2749
|
+
const msg = String(error.message);
|
|
2750
|
+
const errorWithCode = error;
|
|
2751
|
+
if (errorWithCode.code) {
|
|
2752
|
+
return `${errorWithCode.code}: ${msg}`;
|
|
2753
|
+
}
|
|
2754
|
+
return msg;
|
|
2755
|
+
}
|
|
2756
|
+
return String(error) || "Unknown error";
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Test database connection
|
|
2760
|
+
*/
|
|
2761
|
+
async testConnection() {
|
|
2762
|
+
if (!this.knexInstance) {
|
|
2763
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2764
|
+
}
|
|
2765
|
+
try {
|
|
2766
|
+
const result = await this.knexInstance.raw("SELECT 2+3 AS result");
|
|
2767
|
+
const isOk = result.rows?.[0]?.result === 5 || result[0]?.[0]?.result === 5;
|
|
2768
|
+
this.logger.debug?.(`[Db] Connection test: ${isOk ? "OK" : "FAILED"}`);
|
|
2769
|
+
return isOk;
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
const errorMsg = this.getErrorMessage(error);
|
|
2772
|
+
this.logger.error?.(`[Db] Connection test failed: ${errorMsg}`);
|
|
2773
|
+
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Attach query profiler to log all queries
|
|
2778
|
+
*/
|
|
2779
|
+
attachProfiler() {
|
|
2780
|
+
if (!this.knexInstance) {
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
this.queriesLog = [];
|
|
2784
|
+
this.knexInstance.queriesLog = this.queriesLog;
|
|
2785
|
+
this.knexInstance.on("query", (query) => {
|
|
2786
|
+
query.__startTime = process.hrtime();
|
|
2787
|
+
});
|
|
2788
|
+
this.knexInstance.on("query-response", (response, query) => {
|
|
2789
|
+
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2790
|
+
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2791
|
+
const logEntry = {
|
|
2792
|
+
sql: query.sql,
|
|
2793
|
+
bindings: query.bindings || [],
|
|
2794
|
+
executionTimeMs
|
|
2795
|
+
};
|
|
2796
|
+
this.queriesLog.push(logEntry);
|
|
2797
|
+
this.logger.debug?.(`[Db] Query: ${query.sql} | Duration: ${executionTimeMs}ms`);
|
|
2798
|
+
});
|
|
2799
|
+
this.knexInstance.on("query-error", (error, query) => {
|
|
2800
|
+
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
/**
|
|
2804
|
+
* Get query log (only available if profiling is enabled)
|
|
2805
|
+
*/
|
|
2806
|
+
getQueryLog() {
|
|
2807
|
+
return [...this.queriesLog];
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Check if a table exists
|
|
2811
|
+
*/
|
|
2812
|
+
async tableExists(tableName) {
|
|
2813
|
+
if (!this.knexInstance) {
|
|
2814
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2815
|
+
}
|
|
2816
|
+
try {
|
|
2817
|
+
return await this.knexInstance.schema.hasTable(tableName);
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
|
|
2820
|
+
throw error;
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Get the underlying Knex instance (for advanced usage)
|
|
2825
|
+
*/
|
|
2826
|
+
getKnex() {
|
|
2827
|
+
if (!this.knexInstance) {
|
|
2828
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
2829
|
+
}
|
|
2830
|
+
return this.knexInstance;
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Get connection status
|
|
2834
|
+
*/
|
|
2835
|
+
isConnectedToDb() {
|
|
2836
|
+
return this.isConnected && this.knexInstance !== null;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
2840
|
+
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
2841
|
+
*/
|
|
2842
|
+
static async init(context, dbNameOrConnectionString) {
|
|
2843
|
+
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
function capitalizeFirstLetter(str) {
|
|
2847
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2848
|
+
}
|
|
2849
|
+
async function dbConnect(context, connectionString, name, dbProfile) {
|
|
2850
|
+
const defs2 = {
|
|
2851
|
+
testDbConnection: "boolean default true",
|
|
2852
|
+
name: "string",
|
|
2853
|
+
poolMin: "number default 2",
|
|
2854
|
+
poolMax: "number default 10",
|
|
2855
|
+
acquireConnectionTimeout: "number default 10000",
|
|
2856
|
+
sslRejectUnauthorized: "boolean default false"
|
|
2857
|
+
};
|
|
2858
|
+
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2859
|
+
const config2 = {
|
|
2860
|
+
connectionString,
|
|
2861
|
+
name: paramsConfig.name || name || "default",
|
|
2862
|
+
testConnection: paramsConfig.testDbConnection,
|
|
2863
|
+
profile: dbProfile ?? false,
|
|
2864
|
+
pool: {
|
|
2865
|
+
min: paramsConfig.poolMin,
|
|
2866
|
+
max: paramsConfig.poolMax
|
|
2867
|
+
},
|
|
2868
|
+
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
2869
|
+
ssl: {
|
|
2870
|
+
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
2871
|
+
},
|
|
2872
|
+
logger: context.logger
|
|
2873
|
+
};
|
|
2874
|
+
try {
|
|
2875
|
+
const db = new Db(config2);
|
|
2876
|
+
context.registerCleanup(async () => {
|
|
2877
|
+
await db.disconnect();
|
|
2878
|
+
context.logger.debug(`[Db] instance "${name || connectionString}" destroyed`);
|
|
2879
|
+
});
|
|
2880
|
+
await db.connect();
|
|
2881
|
+
context.logger.debug(`[Db] instance "${name || connectionString}" initialized`);
|
|
2882
|
+
return db;
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
if (error instanceof ParamError) {
|
|
2885
|
+
throw error;
|
|
2886
|
+
}
|
|
2887
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2888
|
+
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
2892
|
+
let dbName;
|
|
2893
|
+
let dbConnectionString;
|
|
2894
|
+
let dbProfile;
|
|
2895
|
+
if (dbNameOrConnectionString) {
|
|
2896
|
+
if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
|
|
2897
|
+
dbName = void 0;
|
|
2898
|
+
dbConnectionString = dbNameOrConnectionString;
|
|
2899
|
+
} else {
|
|
2900
|
+
dbName = dbNameOrConnectionString;
|
|
2901
|
+
}
|
|
2902
|
+
} else {
|
|
2903
|
+
const defs2 = {
|
|
2904
|
+
dbName: "string",
|
|
2905
|
+
dbConnectionString: "string",
|
|
2906
|
+
dbProfile: "boolean default false"
|
|
2907
|
+
};
|
|
2908
|
+
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2909
|
+
dbName = paramsConfig.dbName;
|
|
2910
|
+
dbConnectionString = paramsConfig.dbConnectionString;
|
|
2911
|
+
dbProfile = paramsConfig.dbProfile;
|
|
2912
|
+
}
|
|
2913
|
+
if (!dbName && !dbConnectionString) {
|
|
2914
|
+
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
2915
|
+
}
|
|
2916
|
+
if (dbName) {
|
|
2917
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2918
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2919
|
+
if (!dbConnectionString) {
|
|
2920
|
+
throw new ParamError(
|
|
2921
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
|
|
2926
|
+
return db;
|
|
2927
|
+
}
|
|
2928
|
+
async function dbInit(context, dbNameOrConnectionString) {
|
|
2929
|
+
return await dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// src/utils/date-utils.ts
|
|
2933
|
+
function isTimestampFolder(folderName) {
|
|
2934
|
+
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2935
|
+
if (!isoRegex.test(folderName)) {
|
|
2936
|
+
return false;
|
|
2937
|
+
}
|
|
2938
|
+
const date = new Date(folderName);
|
|
2939
|
+
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// src/utils/fs-utils.ts
|
|
2943
|
+
import fs from "fs";
|
|
2944
|
+
import path from "path";
|
|
2945
|
+
async function ensurePath(...pathParts) {
|
|
2946
|
+
const fullPath = path.resolve(...pathParts);
|
|
2947
|
+
if (!fs.existsSync(fullPath)) {
|
|
2948
|
+
await fs.promises.mkdir(fullPath, { recursive: true });
|
|
2949
|
+
}
|
|
2950
|
+
return fullPath;
|
|
2951
|
+
}
|
|
2952
|
+
function getFileExtension(dataType) {
|
|
2953
|
+
switch (dataType) {
|
|
2954
|
+
case "json-array":
|
|
2955
|
+
case "json-object":
|
|
2956
|
+
return "json";
|
|
2957
|
+
case "text":
|
|
2958
|
+
return "txt";
|
|
2959
|
+
case "xml":
|
|
2960
|
+
return "xml";
|
|
2961
|
+
default:
|
|
2962
|
+
return "json";
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/utils/os-utils.ts
|
|
2967
|
+
import fs2 from "fs";
|
|
2968
|
+
import path2 from "path";
|
|
2969
|
+
import { execSync } from "child_process";
|
|
2970
|
+
function getFreeDiskSpace(targetPath) {
|
|
2971
|
+
try {
|
|
2972
|
+
let pathToCheck = targetPath;
|
|
2973
|
+
if (!fs2.existsSync(targetPath)) {
|
|
2974
|
+
const parentDir = path2.dirname(targetPath);
|
|
2975
|
+
if (fs2.existsSync(parentDir)) {
|
|
2976
|
+
pathToCheck = parentDir;
|
|
2977
|
+
} else {
|
|
2978
|
+
pathToCheck = process.platform === "win32" ? "C:\\" : "/";
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
if (process.platform === "win32") {
|
|
2982
|
+
return null;
|
|
2983
|
+
} else {
|
|
2984
|
+
const stdout = execSync(`df -k "${pathToCheck}"`, { encoding: "utf8" });
|
|
2985
|
+
const lines = stdout.trim().split("\n");
|
|
2986
|
+
const parts = lines[1].split(/\s+/);
|
|
2987
|
+
const freeKb = parseInt(parts[3], 10);
|
|
2988
|
+
return freeKb * 1024;
|
|
2989
|
+
}
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// src/utils/format-utils.ts
|
|
2996
|
+
function bytesToHumanReadable(bytes) {
|
|
2997
|
+
if (bytes === 0) return "0 B";
|
|
2998
|
+
const k = 1024;
|
|
2999
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
|
3000
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
3001
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/utils/core-utils.ts
|
|
3005
|
+
function sleepMs(ms) {
|
|
3006
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3007
|
+
}
|
|
3008
|
+
function toJsonColumn(value) {
|
|
3009
|
+
if (value === void 0 || value === null) return null;
|
|
3010
|
+
return JSON.stringify(value);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// src/tasks/taskUtils.ts
|
|
3014
|
+
import { randomUUID } from "crypto";
|
|
3015
|
+
function getDb(context) {
|
|
3016
|
+
const db = context.db;
|
|
3017
|
+
if (!db) {
|
|
3018
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
3019
|
+
}
|
|
3020
|
+
return db;
|
|
3021
|
+
}
|
|
3022
|
+
function queueToTableNames(queue) {
|
|
3023
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
3024
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
3025
|
+
}
|
|
3026
|
+
return {
|
|
3027
|
+
tasksTable: queue,
|
|
3028
|
+
historyTable: `${queue}_history`
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
async function ensureTaskTables(context, options = {}) {
|
|
3032
|
+
const queue = options.queue ?? "tasks";
|
|
3033
|
+
const recreate = options.recreate ?? false;
|
|
3034
|
+
const db = getDb(context);
|
|
3035
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
3036
|
+
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3037
|
+
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3038
|
+
if (recreate) {
|
|
3039
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
3040
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
3041
|
+
}
|
|
3042
|
+
if (needsTasks) {
|
|
3043
|
+
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3044
|
+
await db.schema.createTable(tasksTable, (t) => {
|
|
3045
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3046
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3047
|
+
t.timestamp("started_at");
|
|
3048
|
+
t.timestamp("completed_at");
|
|
3049
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
3050
|
+
t.text("schedule");
|
|
3051
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3052
|
+
t.text("target").notNullable();
|
|
3053
|
+
t.text("task").notNullable();
|
|
3054
|
+
t.json("params");
|
|
3055
|
+
t.text("opid");
|
|
3056
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
3057
|
+
t.text("progress");
|
|
3058
|
+
t.boolean("success");
|
|
3059
|
+
t.json("results");
|
|
3060
|
+
});
|
|
3061
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
3062
|
+
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
3063
|
+
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
3064
|
+
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
3068
|
+
if (!tasksHasOpid) {
|
|
3069
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
3070
|
+
t.text("opid");
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
3074
|
+
if (!tasksHasPausedAt) {
|
|
3075
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
3076
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
if (needsHistory) {
|
|
3080
|
+
await db.schema.createTable(historyTable, (t) => {
|
|
3081
|
+
t.uuid("id").notNullable();
|
|
3082
|
+
t.timestamp("created_at").notNullable();
|
|
3083
|
+
t.timestamp("started_at");
|
|
3084
|
+
t.timestamp("completed_at");
|
|
3085
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
3086
|
+
t.text("schedule");
|
|
3087
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3088
|
+
t.text("target").notNullable();
|
|
3089
|
+
t.text("task").notNullable();
|
|
3090
|
+
t.json("params");
|
|
3091
|
+
t.text("opid");
|
|
3092
|
+
t.text("progress");
|
|
3093
|
+
t.boolean("success");
|
|
3094
|
+
t.json("results");
|
|
3095
|
+
});
|
|
3096
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
3097
|
+
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
3098
|
+
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
|
|
3102
|
+
if (!historyHasOpid) {
|
|
3103
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
3104
|
+
t.text("opid");
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
3109
|
+
const db = getDb(context);
|
|
3110
|
+
await db(tasksTable).where({ id: taskId }).update({
|
|
3111
|
+
progress: typeof progress === "string" ? progress : JSON.stringify(progress)
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/filedatabase/index.ts
|
|
3116
|
+
import fs3 from "fs";
|
|
3117
|
+
import path3 from "path";
|
|
3118
|
+
|
|
3119
|
+
// src/filedatabase/serializers.ts
|
|
3120
|
+
function detectDataType(data) {
|
|
3121
|
+
if (Array.isArray(data)) {
|
|
3122
|
+
return "json-array";
|
|
3123
|
+
} else if (typeof data === "object" && data !== null) {
|
|
3124
|
+
return "json-object";
|
|
3125
|
+
} else if (typeof data === "string") {
|
|
3126
|
+
const trimmed = data.trim();
|
|
3127
|
+
if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) {
|
|
3128
|
+
return "xml";
|
|
3129
|
+
}
|
|
3130
|
+
return "text";
|
|
3131
|
+
} else {
|
|
3132
|
+
return "text";
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function serializeData(data) {
|
|
3136
|
+
const dataType = detectDataType(data);
|
|
3137
|
+
if (dataType === "json-array" || dataType === "json-object") {
|
|
3138
|
+
return JSON.stringify(data, null, 4);
|
|
3139
|
+
} else {
|
|
3140
|
+
return String(data);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function deserializeData(rawData, dataType) {
|
|
3144
|
+
if (dataType === "json-array" || dataType === "json-object") {
|
|
3145
|
+
return JSON.parse(rawData);
|
|
3146
|
+
} else {
|
|
3147
|
+
return rawData;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/filedatabase/index.ts
|
|
3152
|
+
var FileDatabase = class _FileDatabase {
|
|
3153
|
+
basePath;
|
|
3154
|
+
namespace;
|
|
3155
|
+
tableName = null;
|
|
3156
|
+
versioned;
|
|
3157
|
+
maxVersions;
|
|
3158
|
+
pageSize;
|
|
3159
|
+
useMetadata;
|
|
3160
|
+
freeSpaceThreshold;
|
|
3161
|
+
logger;
|
|
3162
|
+
// Current operation state
|
|
3163
|
+
currentVersion = null;
|
|
3164
|
+
currentVersionFolder = null;
|
|
3165
|
+
currentFileNumber = 0;
|
|
3166
|
+
currentRecord = 0;
|
|
3167
|
+
hasReadFirstPage = false;
|
|
3168
|
+
lastFileData = null;
|
|
3169
|
+
metadata;
|
|
3170
|
+
// Synopsis calculation functions
|
|
3171
|
+
fileSynopsisFunction = null;
|
|
3172
|
+
versionSynopsisFunction = null;
|
|
3173
|
+
/**
|
|
3174
|
+
* Constructor - accepts context as first parameter (new pattern)
|
|
3175
|
+
* or config object (legacy pattern for backward compatibility)
|
|
3176
|
+
*/
|
|
3177
|
+
constructor(contextOrConfig, options) {
|
|
3178
|
+
let config2;
|
|
3179
|
+
if (contextOrConfig && typeof contextOrConfig === "object" && "params" in contextOrConfig) {
|
|
3180
|
+
const context = contextOrConfig;
|
|
3181
|
+
const opts = options || {};
|
|
3182
|
+
const defs2 = {
|
|
3183
|
+
basePath: "string default ./data",
|
|
3184
|
+
namespace: "string default default",
|
|
3185
|
+
tableName: "string",
|
|
3186
|
+
maxVersions: "number default 5",
|
|
3187
|
+
pageSize: "number default 5000"
|
|
3188
|
+
};
|
|
3189
|
+
const discovered = context.params.getAllForModule(defs2);
|
|
3190
|
+
config2 = { ...discovered, ...opts, logger: context.logger };
|
|
3191
|
+
} else {
|
|
3192
|
+
config2 = contextOrConfig;
|
|
3193
|
+
}
|
|
3194
|
+
if (!config2.basePath) {
|
|
3195
|
+
throw new ParamError("[FileDatabase] basePath is required");
|
|
3196
|
+
}
|
|
3197
|
+
this.basePath = config2.basePath;
|
|
3198
|
+
this.namespace = config2.namespace || "default";
|
|
3199
|
+
this.tableName = config2.tableName || null;
|
|
3200
|
+
this.versioned = config2.versioned ?? true;
|
|
3201
|
+
this.maxVersions = config2.maxVersions || 5;
|
|
3202
|
+
this.pageSize = config2.pageSize || 5e3;
|
|
3203
|
+
this.useMetadata = config2.useMetadata !== false;
|
|
3204
|
+
this.freeSpaceThreshold = config2.freeSpaceThreshold || 100 * 1024 * 1024;
|
|
3205
|
+
this.logger = config2.logger || console;
|
|
3206
|
+
this.metadata = this.getDefaultMetadata();
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Initialize FileDatabase from context and options.
|
|
3210
|
+
* Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
|
|
3211
|
+
*/
|
|
3212
|
+
static init(context, options) {
|
|
3213
|
+
return new _FileDatabase(context, options ?? {});
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Get default metadata structure
|
|
3217
|
+
*/
|
|
3218
|
+
getDefaultMetadata() {
|
|
3219
|
+
return {
|
|
3220
|
+
version: this.currentVersion || null,
|
|
3221
|
+
files: [],
|
|
3222
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3223
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3224
|
+
totalRecords: 0,
|
|
3225
|
+
synopsis: null,
|
|
3226
|
+
dataType: null
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Get the destination path (basePath/namespace/tableName[/version])
|
|
3231
|
+
*/
|
|
3232
|
+
getDestinationPath(version) {
|
|
3233
|
+
const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
|
|
3234
|
+
if (errors.length) {
|
|
3235
|
+
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3236
|
+
}
|
|
3237
|
+
let parts = [this.basePath, this.namespace];
|
|
3238
|
+
if (this.tableName) {
|
|
3239
|
+
parts.push(...this.tableName.split("/"));
|
|
3240
|
+
}
|
|
3241
|
+
if (this.versioned && version) {
|
|
3242
|
+
parts.push(version);
|
|
3243
|
+
}
|
|
3244
|
+
return path3.resolve(...parts);
|
|
3245
|
+
}
|
|
3246
|
+
/**
|
|
3247
|
+
* Set current version and version folder
|
|
3248
|
+
*/
|
|
3249
|
+
async setCurrentVersion(version) {
|
|
3250
|
+
this.currentVersion = version;
|
|
3251
|
+
this.currentVersionFolder = await ensurePath(this.getDestinationPath(), version);
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Create a new version folder with comprehensive timestamp logic
|
|
3255
|
+
* Only works in versioned mode
|
|
3256
|
+
*/
|
|
3257
|
+
async makeNewVersion() {
|
|
3258
|
+
if (!this.versioned) {
|
|
3259
|
+
throw new FileDatabaseError("makeNewVersion() only works in versioned mode");
|
|
3260
|
+
}
|
|
3261
|
+
this.metadata = this.getDefaultMetadata();
|
|
3262
|
+
const existingVersions = await this.getVersions();
|
|
3263
|
+
let versionName;
|
|
3264
|
+
if (existingVersions.length > 0) {
|
|
3265
|
+
const maxTimestamp = existingVersions.reduce((max, version) => {
|
|
3266
|
+
const versionDate = new Date(version.replace("Z", ""));
|
|
3267
|
+
const maxDate2 = new Date(max.replace("Z", ""));
|
|
3268
|
+
return versionDate > maxDate2 ? version : max;
|
|
3269
|
+
});
|
|
3270
|
+
const maxDate = new Date(maxTimestamp.replace("Z", ""));
|
|
3271
|
+
const nextDate = new Date(maxDate.getTime() + 1e3);
|
|
3272
|
+
versionName = nextDate.toISOString().split(".")[0] + "Z";
|
|
3273
|
+
} else {
|
|
3274
|
+
const now = /* @__PURE__ */ new Date();
|
|
3275
|
+
versionName = now.toISOString().split(".")[0] + "Z";
|
|
3276
|
+
}
|
|
3277
|
+
await this.setCurrentVersion(versionName);
|
|
3278
|
+
this.currentFileNumber = 0;
|
|
3279
|
+
const versions = await this.getVersions();
|
|
3280
|
+
while (versions.length > this.maxVersions) {
|
|
3281
|
+
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
3282
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
3283
|
+
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
3284
|
+
}
|
|
3285
|
+
return versionName;
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* Get list of all versions (sorted chronologically)
|
|
3289
|
+
* Only works in versioned mode
|
|
3290
|
+
*/
|
|
3291
|
+
async getVersions() {
|
|
3292
|
+
if (!this.versioned) {
|
|
3293
|
+
return [];
|
|
3294
|
+
}
|
|
3295
|
+
const destPath = this.getDestinationPath();
|
|
3296
|
+
try {
|
|
3297
|
+
await ensurePath(destPath);
|
|
3298
|
+
const items = await fs3.promises.readdir(destPath);
|
|
3299
|
+
const versions = items.filter((item) => {
|
|
3300
|
+
const itemPath = path3.join(destPath, item);
|
|
3301
|
+
const stat = fs3.statSync(itemPath);
|
|
3302
|
+
return stat.isDirectory() && isTimestampFolder(item);
|
|
3303
|
+
});
|
|
3304
|
+
return versions.sort();
|
|
3305
|
+
} catch (error) {
|
|
3306
|
+
return [];
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Get the latest version (most recent timestamp)
|
|
3311
|
+
* Only works in versioned mode
|
|
3312
|
+
* @returns Latest version string or null if no versions
|
|
3313
|
+
*/
|
|
3314
|
+
async getLatestVersion() {
|
|
3315
|
+
if (!this.versioned) {
|
|
3316
|
+
throw new FileDatabaseError("getLatestVersion() only works in versioned mode");
|
|
3317
|
+
}
|
|
3318
|
+
const versions = await this.getVersions();
|
|
3319
|
+
if (versions.length === 0) {
|
|
3320
|
+
return null;
|
|
3321
|
+
}
|
|
3322
|
+
return versions[versions.length - 1];
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Check if any data exists in this table
|
|
3326
|
+
* Works for both versioned and non-versioned modes
|
|
3327
|
+
* @returns true if data exists
|
|
3328
|
+
*/
|
|
3329
|
+
async hasData() {
|
|
3330
|
+
const tablePath = this.getDestinationPath();
|
|
3331
|
+
if (!fs3.existsSync(tablePath)) {
|
|
3332
|
+
return false;
|
|
3333
|
+
}
|
|
3334
|
+
if (this.versioned) {
|
|
3335
|
+
const versions = await this.getVersions();
|
|
3336
|
+
return versions.length > 0;
|
|
3337
|
+
} else {
|
|
3338
|
+
const items = await fs3.promises.readdir(tablePath);
|
|
3339
|
+
return items.some(
|
|
3340
|
+
(item) => item === "metadata.json" || item.match(/^\d{6}\.(json|txt|xml)$/) || item.endsWith(".json")
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Auto-detect the data format in this table
|
|
3346
|
+
* Used when reading existing data
|
|
3347
|
+
* @returns Format detection result
|
|
3348
|
+
*/
|
|
3349
|
+
async detectDataFormat() {
|
|
3350
|
+
const tablePath = this.getDestinationPath();
|
|
3351
|
+
if (!fs3.existsSync(tablePath)) {
|
|
3352
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
3353
|
+
}
|
|
3354
|
+
const items = await fs3.promises.readdir(tablePath);
|
|
3355
|
+
if (items.includes("metadata.json")) {
|
|
3356
|
+
const metadata = JSON.parse(
|
|
3357
|
+
await fs3.promises.readFile(path3.join(tablePath, "metadata.json"), "utf8")
|
|
3358
|
+
);
|
|
3359
|
+
return {
|
|
3360
|
+
versioned: false,
|
|
3361
|
+
hasMetadata: true,
|
|
3362
|
+
dataType: metadata.dataType || null
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
const versionFolders = items.filter((item) => {
|
|
3366
|
+
const itemPath = path3.join(tablePath, item);
|
|
3367
|
+
const stat = fs3.statSync(itemPath);
|
|
3368
|
+
return stat.isDirectory() && isTimestampFolder(item);
|
|
3369
|
+
});
|
|
3370
|
+
if (versionFolders.length > 0) {
|
|
3371
|
+
const latestVersion = versionFolders.sort().pop();
|
|
3372
|
+
const versionMetadataPath = path3.join(tablePath, latestVersion, "metadata.json");
|
|
3373
|
+
return {
|
|
3374
|
+
versioned: true,
|
|
3375
|
+
hasMetadata: fs3.existsSync(versionMetadataPath),
|
|
3376
|
+
dataType: null
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
const dataFiles = items.filter((f) => f.match(/^\d{6}\.(json|txt|xml)$/));
|
|
3380
|
+
if (dataFiles.length > 0) {
|
|
3381
|
+
return {
|
|
3382
|
+
versioned: false,
|
|
3383
|
+
hasMetadata: false,
|
|
3384
|
+
dataType: null
|
|
3385
|
+
};
|
|
3386
|
+
}
|
|
3387
|
+
return { versioned: false, hasMetadata: false, dataType: null };
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Load metadata from JSON file
|
|
3391
|
+
*/
|
|
3392
|
+
async loadMetadataJson(version) {
|
|
3393
|
+
const metadataFile = path3.join(this.getDestinationPath(), version, "metadata.json");
|
|
3394
|
+
if (fs3.existsSync(metadataFile)) {
|
|
3395
|
+
try {
|
|
3396
|
+
const rawData = await fs3.promises.readFile(metadataFile, "utf8");
|
|
3397
|
+
return JSON.parse(rawData);
|
|
3398
|
+
} catch (e) {
|
|
3399
|
+
throw new FileDatabaseError(`Failed to read metadata for version "${version}": ${e.message}`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Build metadata by scanning files in a version folder (backward compatibility)
|
|
3406
|
+
* Reads all files to get accurate counts - used when synopsis calculation is needed
|
|
3407
|
+
*/
|
|
3408
|
+
async figureMetadataFromVersionFiles(version) {
|
|
3409
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3410
|
+
if (!fs3.existsSync(versionPath)) {
|
|
3411
|
+
return this.getDefaultMetadata();
|
|
3412
|
+
}
|
|
3413
|
+
const files = (await fs3.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
|
|
3414
|
+
const metadata = this.getDefaultMetadata();
|
|
3415
|
+
metadata.version = version;
|
|
3416
|
+
metadata.files = [];
|
|
3417
|
+
let totalRecords = 0;
|
|
3418
|
+
let detectedDataType = null;
|
|
3419
|
+
for (let i = 0; i < files.length; i++) {
|
|
3420
|
+
const fileName = files[i];
|
|
3421
|
+
const filePath = path3.join(versionPath, fileName);
|
|
3422
|
+
try {
|
|
3423
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3424
|
+
const extension = path3.extname(fileName).toLowerCase();
|
|
3425
|
+
let dataType = "text";
|
|
3426
|
+
if (extension === ".json") {
|
|
3427
|
+
dataType = "json-array";
|
|
3428
|
+
} else if (extension === ".xml") {
|
|
3429
|
+
dataType = "xml";
|
|
3430
|
+
}
|
|
3431
|
+
const fileData = deserializeData(rawData, dataType);
|
|
3432
|
+
const recordsCount = Array.isArray(fileData) ? fileData.length : 1;
|
|
3433
|
+
if (detectedDataType === null) {
|
|
3434
|
+
detectedDataType = detectDataType(fileData);
|
|
3435
|
+
}
|
|
3436
|
+
const fileInfo = {
|
|
3437
|
+
number: i + 1,
|
|
3438
|
+
recordsCount,
|
|
3439
|
+
fileName
|
|
3440
|
+
};
|
|
3441
|
+
metadata.files.push(fileInfo);
|
|
3442
|
+
totalRecords += recordsCount;
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
this.logger.error?.(`[FileDatabase] Failed to read file ${fileName}: ${error.message}`);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
metadata.totalRecords = totalRecords;
|
|
3448
|
+
metadata.dataType = detectedDataType;
|
|
3449
|
+
return metadata;
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Build metadata optimized - only reads first and last files
|
|
3453
|
+
* Assumes all middle files have the same record count as the first file
|
|
3454
|
+
* Much faster for large datasets with many files
|
|
3455
|
+
*/
|
|
3456
|
+
async buildMetadataOptimized(version) {
|
|
3457
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3458
|
+
if (!fs3.existsSync(versionPath)) {
|
|
3459
|
+
return this.getDefaultMetadata();
|
|
3460
|
+
}
|
|
3461
|
+
const files = (await fs3.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
|
|
3462
|
+
if (files.length === 0) {
|
|
3463
|
+
return this.getDefaultMetadata();
|
|
3464
|
+
}
|
|
3465
|
+
const metadata = this.getDefaultMetadata();
|
|
3466
|
+
metadata.version = version;
|
|
3467
|
+
metadata.files = files.map((fileName, index) => ({
|
|
3468
|
+
number: index + 1,
|
|
3469
|
+
recordsCount: 0,
|
|
3470
|
+
fileName
|
|
3471
|
+
}));
|
|
3472
|
+
const firstFile = metadata.files[0];
|
|
3473
|
+
const firstFilePath = path3.join(versionPath, firstFile.fileName);
|
|
3474
|
+
const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
|
|
3475
|
+
let firstFileData;
|
|
3476
|
+
try {
|
|
3477
|
+
firstFileData = JSON.parse(firstFileRaw);
|
|
3478
|
+
} catch (e) {
|
|
3479
|
+
firstFileData = firstFileRaw;
|
|
3480
|
+
}
|
|
3481
|
+
metadata.dataType = detectDataType(firstFileData);
|
|
3482
|
+
if (metadata.dataType === "json-array") {
|
|
3483
|
+
const firstFileCount = Array.isArray(firstFileData) ? firstFileData.length : 1;
|
|
3484
|
+
firstFile.recordsCount = firstFileCount;
|
|
3485
|
+
for (let i = 1; i < metadata.files.length - 1; i++) {
|
|
3486
|
+
metadata.files[i].recordsCount = firstFileCount;
|
|
3487
|
+
}
|
|
3488
|
+
if (files.length > 1) {
|
|
3489
|
+
const lastFile = metadata.files[metadata.files.length - 1];
|
|
3490
|
+
const lastFilePath = path3.join(versionPath, lastFile.fileName);
|
|
3491
|
+
const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
|
|
3492
|
+
const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
|
|
3493
|
+
lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
|
|
3494
|
+
}
|
|
3495
|
+
metadata.totalRecords = metadata.files.reduce((sum, file) => sum + file.recordsCount, 0);
|
|
3496
|
+
} else {
|
|
3497
|
+
metadata.files.forEach((file) => {
|
|
3498
|
+
file.recordsCount = 1;
|
|
3499
|
+
});
|
|
3500
|
+
metadata.totalRecords = files.length;
|
|
3501
|
+
}
|
|
3502
|
+
return metadata;
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Figure out metadata - tries JSON first, then builds from files
|
|
3506
|
+
* Uses optimized building when no synopsis calculation is needed
|
|
3507
|
+
*/
|
|
3508
|
+
async figureMetadata(version, useOptimized = true) {
|
|
3509
|
+
if (this.useMetadata) {
|
|
3510
|
+
const metadata = await this.loadMetadataJson(version);
|
|
3511
|
+
if (metadata) {
|
|
3512
|
+
return metadata;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
if (useOptimized && !this.fileSynopsisFunction && !this.versionSynopsisFunction) {
|
|
3516
|
+
return await this.buildMetadataOptimized(version);
|
|
3517
|
+
}
|
|
3518
|
+
return await this.figureMetadataFromVersionFiles(version);
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Load version metadata (main entry point for loading)
|
|
3522
|
+
*/
|
|
3523
|
+
async loadVersionMetadata(version) {
|
|
3524
|
+
const metadata = await this.figureMetadata(version);
|
|
3525
|
+
this.metadata = metadata;
|
|
3526
|
+
return metadata;
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Save version metadata to file
|
|
3530
|
+
*/
|
|
3531
|
+
async saveVersionMetadata(metadata) {
|
|
3532
|
+
if (!this.useMetadata) {
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const metadataToSave = metadata || this.metadata;
|
|
3536
|
+
let metadataFile;
|
|
3537
|
+
if (this.versioned) {
|
|
3538
|
+
if (!this.currentVersion) {
|
|
3539
|
+
return;
|
|
3540
|
+
}
|
|
3541
|
+
metadataFile = path3.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
3542
|
+
} else {
|
|
3543
|
+
metadataFile = path3.join(this.getDestinationPath(), "metadata.json");
|
|
3544
|
+
}
|
|
3545
|
+
await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Create a new file entry in metadata
|
|
3549
|
+
*/
|
|
3550
|
+
makeNewFile() {
|
|
3551
|
+
this.currentFileNumber = (this.currentFileNumber || 0) + 1;
|
|
3552
|
+
const dataType = this.metadata.dataType || "json-array";
|
|
3553
|
+
const fileEntry = {
|
|
3554
|
+
number: this.currentFileNumber,
|
|
3555
|
+
recordsCount: 0,
|
|
3556
|
+
fileName: `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(dataType)}`
|
|
3557
|
+
};
|
|
3558
|
+
this.metadata.files.push(fileEntry);
|
|
3559
|
+
this.lastFileData = null;
|
|
3560
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Figure out what data to write and which file to use (for pagination)
|
|
3564
|
+
* @param data - Data to write
|
|
3565
|
+
* @param targetFileIndex - Optional index of existing file to overwrite (when customMetadata matches)
|
|
3566
|
+
* @param forceNewFile - If true, always create a new file (when customMetadata provided but no match)
|
|
3567
|
+
*/
|
|
3568
|
+
figureOutDataAndFileToWrite(data, targetFileIndex = null, forceNewFile = false) {
|
|
3569
|
+
let dataToWrite;
|
|
3570
|
+
let dataLeftOver;
|
|
3571
|
+
const incomingDataType = detectDataType(data);
|
|
3572
|
+
if (this.metadata.dataType !== incomingDataType) {
|
|
3573
|
+
this.metadata.dataType = incomingDataType;
|
|
3574
|
+
}
|
|
3575
|
+
if (targetFileIndex !== null && targetFileIndex < this.metadata.files.length) {
|
|
3576
|
+
const targetFile = this.metadata.files[targetFileIndex];
|
|
3577
|
+
if (!Array.isArray(data)) {
|
|
3578
|
+
dataToWrite = data;
|
|
3579
|
+
dataLeftOver = null;
|
|
3580
|
+
return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
|
|
3581
|
+
} else {
|
|
3582
|
+
dataToWrite = data.slice(0, this.pageSize);
|
|
3583
|
+
dataLeftOver = data.slice(this.pageSize);
|
|
3584
|
+
this.lastFileData = dataToWrite;
|
|
3585
|
+
return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
let newlyCreatedFileIndex = null;
|
|
3589
|
+
if (forceNewFile) {
|
|
3590
|
+
const filesBeforeCreate = this.metadata.files.length;
|
|
3591
|
+
this.makeNewFile();
|
|
3592
|
+
newlyCreatedFileIndex = filesBeforeCreate;
|
|
3593
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
3594
|
+
} else if (this.metadata.files.length === 0) {
|
|
3595
|
+
this.makeNewFile();
|
|
3596
|
+
}
|
|
3597
|
+
const lastFile = this.metadata.files[this.metadata.files.length - 1];
|
|
3598
|
+
const lastFileRecordsCount = lastFile.recordsCount;
|
|
3599
|
+
if (forceNewFile && newlyCreatedFileIndex !== null) {
|
|
3600
|
+
const newlyCreatedFile = this.metadata.files[newlyCreatedFileIndex];
|
|
3601
|
+
if (newlyCreatedFile && newlyCreatedFile.fileName !== lastFile.fileName) {
|
|
3602
|
+
this.logger.warn?.(`[FileDatabase] Warning: Newly created file ${newlyCreatedFile.fileName} doesn't match last file ${lastFile.fileName}`);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
if (!Array.isArray(data) && !forceNewFile) {
|
|
3606
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3607
|
+
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3608
|
+
if (lastFileExtension !== expectedExtension) {
|
|
3609
|
+
if (lastFileRecordsCount > 0) {
|
|
3610
|
+
this.makeNewFile();
|
|
3611
|
+
} else {
|
|
3612
|
+
lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
} else if (!Array.isArray(data) && forceNewFile) {
|
|
3616
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3617
|
+
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3618
|
+
if (lastFileExtension !== expectedExtension) {
|
|
3619
|
+
lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
if (Array.isArray(data)) {
|
|
3623
|
+
if (forceNewFile) {
|
|
3624
|
+
dataToWrite = data.slice(0, this.pageSize);
|
|
3625
|
+
dataLeftOver = data.slice(this.pageSize);
|
|
3626
|
+
this.lastFileData = dataToWrite;
|
|
3627
|
+
} else if (lastFileRecordsCount < this.pageSize) {
|
|
3628
|
+
dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
|
|
3629
|
+
dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
|
|
3630
|
+
this.lastFileData = dataToWrite;
|
|
3631
|
+
} else {
|
|
3632
|
+
this.makeNewFile();
|
|
3633
|
+
dataToWrite = data.slice(0, this.pageSize);
|
|
3634
|
+
dataLeftOver = data.slice(this.pageSize);
|
|
3635
|
+
this.lastFileData = dataToWrite;
|
|
3636
|
+
}
|
|
3637
|
+
} else {
|
|
3638
|
+
dataToWrite = data;
|
|
3639
|
+
dataLeftOver = null;
|
|
3640
|
+
}
|
|
3641
|
+
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
3642
|
+
this.logger.silly?.(
|
|
3643
|
+
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
3644
|
+
);
|
|
3645
|
+
return { dataToWrite, dataLeftOver, fileName };
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Calculate file-level synopsis if function is set
|
|
3649
|
+
*/
|
|
3650
|
+
calculateFileSynopsis(data, fileIndex = this.metadata.files.length - 1) {
|
|
3651
|
+
if (!this.fileSynopsisFunction) {
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
const fileInfo = this.metadata.files[fileIndex];
|
|
3655
|
+
const enhancedFileInfo = this.fileSynopsisFunction(fileInfo, data);
|
|
3656
|
+
this.metadata.files[fileIndex] = enhancedFileInfo;
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Calculate version-level synopsis if function is set
|
|
3660
|
+
*/
|
|
3661
|
+
calculateVersionSynopsis() {
|
|
3662
|
+
if (!this.versionSynopsisFunction) {
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
const enhancedMetadata = this.versionSynopsisFunction(this.metadata);
|
|
3666
|
+
this.metadata = enhancedMetadata;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Update metadata after writing data
|
|
3670
|
+
*/
|
|
3671
|
+
updateMetadata(dataToWrite, fileName, customMetadata) {
|
|
3672
|
+
let currentFile;
|
|
3673
|
+
if (fileName) {
|
|
3674
|
+
const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
|
|
3675
|
+
if (!foundFile) {
|
|
3676
|
+
this.logger.warn?.(`[FileDatabase] File ${fileName} not found in metadata, using last file`);
|
|
3677
|
+
currentFile = this.metadata.files[this.metadata.files.length - 1];
|
|
3678
|
+
} else {
|
|
3679
|
+
currentFile = foundFile;
|
|
3680
|
+
}
|
|
3681
|
+
} else {
|
|
3682
|
+
currentFile = this.metadata.files[this.metadata.files.length - 1];
|
|
3683
|
+
}
|
|
3684
|
+
const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
|
|
3685
|
+
currentFile.recordsCount = recordsCount;
|
|
3686
|
+
if (customMetadata) {
|
|
3687
|
+
Object.assign(currentFile, customMetadata);
|
|
3688
|
+
}
|
|
3689
|
+
const fileIndex = this.metadata.files.indexOf(currentFile);
|
|
3690
|
+
if (fileIndex !== -1) {
|
|
3691
|
+
this.calculateFileSynopsis(dataToWrite, fileIndex);
|
|
3692
|
+
}
|
|
3693
|
+
this.metadata.version = this.currentVersion;
|
|
3694
|
+
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3695
|
+
this.metadata.dataType = detectDataType(dataToWrite);
|
|
3696
|
+
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
3697
|
+
this.logger.silly?.(
|
|
3698
|
+
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
/**
|
|
3702
|
+
* Safe write with disk space check
|
|
3703
|
+
*/
|
|
3704
|
+
async safeWrite(filePath, data) {
|
|
3705
|
+
const serializedData = serializeData(data);
|
|
3706
|
+
const dir = path3.dirname(filePath);
|
|
3707
|
+
const requiredBytes = Buffer.byteLength(serializedData, "utf8");
|
|
3708
|
+
const freeBytes = getFreeDiskSpace(dir);
|
|
3709
|
+
if (freeBytes !== null) {
|
|
3710
|
+
if (freeBytes < requiredBytes) {
|
|
3711
|
+
throw new FileDatabaseError(
|
|
3712
|
+
`Not enough disk space. Required: ${bytesToHumanReadable(requiredBytes)}, Free: ${bytesToHumanReadable(freeBytes)}`
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
if (freeBytes < this.freeSpaceThreshold) {
|
|
3716
|
+
this.logger.warn?.(`Low disk space warning: only ${bytesToHumanReadable(freeBytes)} left`);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
try {
|
|
3720
|
+
await fs3.promises.writeFile(filePath, serializedData, "utf8");
|
|
3721
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Prepare the instance for read or write operations
|
|
3728
|
+
* This discovers state and sets up internal members based on mode and current data
|
|
3729
|
+
*/
|
|
3730
|
+
async prepare({ write, read, version }) {
|
|
3731
|
+
if (write) {
|
|
3732
|
+
if (this.versioned) {
|
|
3733
|
+
if (this.currentVersion === null) {
|
|
3734
|
+
await this.makeNewVersion();
|
|
3735
|
+
this.metadata = this.getDefaultMetadata();
|
|
3736
|
+
this.metadata.version = this.currentVersion;
|
|
3737
|
+
this.makeNewFile();
|
|
3738
|
+
} else {
|
|
3739
|
+
if (!this.metadata.files.length) {
|
|
3740
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
3741
|
+
if (this.metadata.files && this.metadata.files.length > 0) {
|
|
3742
|
+
this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
|
|
3743
|
+
} else {
|
|
3744
|
+
this.currentFileNumber = 0;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
} else {
|
|
3749
|
+
await ensurePath(this.getDestinationPath());
|
|
3750
|
+
if (this.useMetadata === true) {
|
|
3751
|
+
const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
|
|
3752
|
+
if (fs3.existsSync(metadataPath)) {
|
|
3753
|
+
try {
|
|
3754
|
+
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
3755
|
+
this.metadata = JSON.parse(rawData);
|
|
3756
|
+
if (this.metadata.files && this.metadata.files.length > 0) {
|
|
3757
|
+
this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
|
|
3758
|
+
} else {
|
|
3759
|
+
this.currentFileNumber = 0;
|
|
3760
|
+
}
|
|
3761
|
+
} catch (e) {
|
|
3762
|
+
this.metadata = this.getDefaultMetadata();
|
|
3763
|
+
this.currentFileNumber = 0;
|
|
3764
|
+
}
|
|
3765
|
+
} else {
|
|
3766
|
+
this.metadata = this.getDefaultMetadata();
|
|
3767
|
+
this.currentFileNumber = 0;
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
this.metadata = this.getDefaultMetadata();
|
|
3771
|
+
this.currentFileNumber = 0;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
} else if (read) {
|
|
3775
|
+
if (this.versioned) {
|
|
3776
|
+
const versions = await this.getVersions();
|
|
3777
|
+
if (versions.length === 0) {
|
|
3778
|
+
throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
|
|
3779
|
+
}
|
|
3780
|
+
if (version) {
|
|
3781
|
+
if (!versions.includes(version)) {
|
|
3782
|
+
throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
|
|
3783
|
+
}
|
|
3784
|
+
await this.setCurrentVersion(version);
|
|
3785
|
+
} else {
|
|
3786
|
+
await this.setCurrentVersion(versions[versions.length - 1]);
|
|
3787
|
+
}
|
|
3788
|
+
if (!this.metadata.files.length) {
|
|
3789
|
+
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
3790
|
+
if (this.metadata.files && this.metadata.files.length > 0) {
|
|
3791
|
+
this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
|
|
3792
|
+
} else {
|
|
3793
|
+
this.currentFileNumber = 0;
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
} else {
|
|
3797
|
+
this.currentVersion = null;
|
|
3798
|
+
if (this.useMetadata === void 0) {
|
|
3799
|
+
const format = await this.detectDataFormat();
|
|
3800
|
+
this.useMetadata = format.hasMetadata;
|
|
3801
|
+
}
|
|
3802
|
+
if (this.useMetadata) {
|
|
3803
|
+
const destPath = this.getDestinationPath();
|
|
3804
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
3805
|
+
if (fs3.existsSync(metadataPath)) {
|
|
3806
|
+
try {
|
|
3807
|
+
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
3808
|
+
this.metadata = JSON.parse(rawData);
|
|
3809
|
+
if (this.metadata.files && this.metadata.files.length > 0) {
|
|
3810
|
+
this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
|
|
3811
|
+
} else {
|
|
3812
|
+
this.currentFileNumber = 0;
|
|
3813
|
+
}
|
|
3814
|
+
} catch (e) {
|
|
3815
|
+
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
3816
|
+
}
|
|
3817
|
+
} else {
|
|
3818
|
+
throw new FileDatabaseError(
|
|
3819
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
} else {
|
|
3823
|
+
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
3824
|
+
if (this.metadata.files && this.metadata.files.length > 0) {
|
|
3825
|
+
this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
|
|
3826
|
+
} else {
|
|
3827
|
+
this.currentFileNumber = 0;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
/**
|
|
3834
|
+
* Write data to the file database
|
|
3835
|
+
*/
|
|
3836
|
+
async write(data, options = {}) {
|
|
3837
|
+
if (options.filename) {
|
|
3838
|
+
const destPath2 = this.getDestinationPath();
|
|
3839
|
+
await ensurePath(destPath2);
|
|
3840
|
+
const filePath = path3.join(destPath2, options.filename);
|
|
3841
|
+
await this.safeWrite(filePath, data);
|
|
3842
|
+
return;
|
|
3843
|
+
}
|
|
3844
|
+
if (options.forceNewVersion && !this.versioned) {
|
|
3845
|
+
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
3846
|
+
}
|
|
3847
|
+
await this.prepare({ write: true });
|
|
3848
|
+
const incomingDataType = detectDataType(data);
|
|
3849
|
+
this.metadata.dataType = incomingDataType;
|
|
3850
|
+
if (options.forceNewVersion) {
|
|
3851
|
+
await this.makeNewVersion();
|
|
3852
|
+
this.metadata = this.getDefaultMetadata();
|
|
3853
|
+
this.metadata.version = this.currentVersion;
|
|
3854
|
+
this.metadata.dataType = incomingDataType;
|
|
3855
|
+
this.makeNewFile();
|
|
3856
|
+
}
|
|
3857
|
+
let targetFileIndex = null;
|
|
3858
|
+
const hasCustomMetadata = options.customMetadata && Object.keys(options.customMetadata).length > 0;
|
|
3859
|
+
if (hasCustomMetadata) {
|
|
3860
|
+
for (let i = 0; i < this.metadata.files.length; i++) {
|
|
3861
|
+
const fileEntry = this.metadata.files[i];
|
|
3862
|
+
const matches = Object.keys(options.customMetadata).every((key) => {
|
|
3863
|
+
return key in fileEntry && fileEntry[key] === options.customMetadata[key];
|
|
3864
|
+
});
|
|
3865
|
+
if (matches) {
|
|
3866
|
+
targetFileIndex = i;
|
|
3867
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
3868
|
+
break;
|
|
3869
|
+
} else {
|
|
3870
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
if (targetFileIndex === null) {
|
|
3874
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
3875
|
+
}
|
|
3876
|
+
} else {
|
|
3877
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
3878
|
+
}
|
|
3879
|
+
if (targetFileIndex !== null) {
|
|
3880
|
+
const targetFile = this.metadata.files[targetFileIndex];
|
|
3881
|
+
this.currentFileNumber = targetFile.number;
|
|
3882
|
+
this.lastFileData = null;
|
|
3883
|
+
this.currentRecord = 0;
|
|
3884
|
+
this.hasReadFirstPage = false;
|
|
3885
|
+
}
|
|
3886
|
+
const forceNewFile = hasCustomMetadata && targetFileIndex === null;
|
|
3887
|
+
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
|
|
3888
|
+
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
3889
|
+
await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
|
|
3890
|
+
this.updateMetadata(dataToWrite, fileName, options.customMetadata);
|
|
3891
|
+
while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
|
|
3892
|
+
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
3893
|
+
await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
3894
|
+
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
|
|
3895
|
+
dataLeftOver = writeContext.dataLeftOver;
|
|
3896
|
+
}
|
|
3897
|
+
this.calculateVersionSynopsis();
|
|
3898
|
+
if (this.useMetadata) {
|
|
3899
|
+
await this.saveVersionMetadata(this.metadata);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Read data from the file database
|
|
3904
|
+
*/
|
|
3905
|
+
async read(options = {}) {
|
|
3906
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
3907
|
+
if (filename) {
|
|
3908
|
+
const destPath = this.getDestinationPath(version);
|
|
3909
|
+
const filePath = path3.join(destPath, filename);
|
|
3910
|
+
try {
|
|
3911
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3912
|
+
return JSON.parse(rawData);
|
|
3913
|
+
} catch (error) {
|
|
3914
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
await this.prepare({ read: true, version });
|
|
3918
|
+
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
3919
|
+
if (isNonPaginatedData) {
|
|
3920
|
+
const file = this.metadata.files[0];
|
|
3921
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
3922
|
+
try {
|
|
3923
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3924
|
+
return deserializeData(rawData, this.metadata.dataType);
|
|
3925
|
+
} catch (error) {
|
|
3926
|
+
throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
let effectivePageSize;
|
|
3930
|
+
if (nextPage && this.hasReadFirstPage) {
|
|
3931
|
+
effectivePageSize = pageSize || this.pageSize;
|
|
3932
|
+
this.currentRecord += effectivePageSize;
|
|
3933
|
+
} else if (!nextPage) {
|
|
3934
|
+
effectivePageSize = pageSize !== void 0 ? pageSize : this.metadata.totalRecords;
|
|
3935
|
+
this.currentRecord = 0;
|
|
3936
|
+
} else {
|
|
3937
|
+
effectivePageSize = pageSize || this.pageSize;
|
|
3938
|
+
}
|
|
3939
|
+
if (this.currentRecord >= this.metadata.totalRecords) {
|
|
3940
|
+
return [];
|
|
3941
|
+
}
|
|
3942
|
+
const result = [];
|
|
3943
|
+
let recordsRead = 0;
|
|
3944
|
+
let currentFileIndex = 0;
|
|
3945
|
+
let currentFileOffset = 0;
|
|
3946
|
+
let totalRecords = 0;
|
|
3947
|
+
for (let i = 0; i < this.metadata.files.length; i++) {
|
|
3948
|
+
const file = this.metadata.files[i];
|
|
3949
|
+
if (this.currentRecord < totalRecords + file.recordsCount) {
|
|
3950
|
+
currentFileIndex = i;
|
|
3951
|
+
currentFileOffset = totalRecords;
|
|
3952
|
+
break;
|
|
3953
|
+
}
|
|
3954
|
+
totalRecords += file.recordsCount;
|
|
3955
|
+
}
|
|
3956
|
+
let cumulativeRecords = currentFileOffset;
|
|
3957
|
+
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
3958
|
+
const file = this.metadata.files[i];
|
|
3959
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
3960
|
+
try {
|
|
3961
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3962
|
+
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
3963
|
+
let startIndex = 0;
|
|
3964
|
+
if (i === currentFileIndex) {
|
|
3965
|
+
startIndex = this.currentRecord - cumulativeRecords;
|
|
3966
|
+
}
|
|
3967
|
+
const endIndex = Math.min(startIndex + (effectivePageSize - recordsRead), fileData.length);
|
|
3968
|
+
const recordsFromThisFile = fileData.slice(startIndex, endIndex);
|
|
3969
|
+
result.push(...recordsFromThisFile);
|
|
3970
|
+
recordsRead += recordsFromThisFile.length;
|
|
3971
|
+
cumulativeRecords += file.recordsCount;
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
if (result.length > 0) {
|
|
3977
|
+
if (nextPage || pageSize !== void 0 && pageSize < this.metadata.totalRecords) {
|
|
3978
|
+
this.hasReadFirstPage = true;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
return result;
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Set the starting record for pagination (1-based index)
|
|
3985
|
+
*/
|
|
3986
|
+
setStartRecord(startRecord) {
|
|
3987
|
+
this.currentRecord = startRecord - 1;
|
|
3988
|
+
this.hasReadFirstPage = false;
|
|
3989
|
+
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Reset read pagination state
|
|
3992
|
+
*/
|
|
3993
|
+
resetPagination() {
|
|
3994
|
+
this.currentRecord = 0;
|
|
3995
|
+
this.hasReadFirstPage = false;
|
|
3996
|
+
}
|
|
3997
|
+
/**
|
|
3998
|
+
* List filenames in the table directory.
|
|
3999
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
4000
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
4001
|
+
*/
|
|
4002
|
+
async listFilenames() {
|
|
4003
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4004
|
+
try {
|
|
4005
|
+
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
4006
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
4007
|
+
} catch (err) {
|
|
4008
|
+
if (err?.code === "ENOENT") return [];
|
|
4009
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* Remove a file from the table directory (catalog mode).
|
|
4014
|
+
* Use with listFilenames() to manage individual files.
|
|
4015
|
+
*/
|
|
4016
|
+
async removeFile(filename) {
|
|
4017
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4018
|
+
const filePath = path3.join(destPath, filename);
|
|
4019
|
+
try {
|
|
4020
|
+
await fs3.promises.unlink(filePath);
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
if (err?.code === "ENOENT") return;
|
|
4023
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
/**
|
|
4027
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
4028
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
4029
|
+
*/
|
|
4030
|
+
async removeFileEntry(filename) {
|
|
4031
|
+
if (this.versioned) {
|
|
4032
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
4033
|
+
}
|
|
4034
|
+
await this.prepare({ read: true });
|
|
4035
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
4036
|
+
if (idx === -1) {
|
|
4037
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
4038
|
+
}
|
|
4039
|
+
const entry = this.metadata.files[idx];
|
|
4040
|
+
const recordsCount = entry.recordsCount || 0;
|
|
4041
|
+
this.metadata.files.splice(idx, 1);
|
|
4042
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
4043
|
+
const destPath = this.getDestinationPath();
|
|
4044
|
+
const filePath = path3.join(destPath, filename);
|
|
4045
|
+
try {
|
|
4046
|
+
await fs3.promises.unlink(filePath);
|
|
4047
|
+
} catch (err) {
|
|
4048
|
+
if (err?.code === "ENOENT") {
|
|
4049
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
4050
|
+
} else {
|
|
4051
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
if (this.useMetadata) {
|
|
4055
|
+
await this.saveVersionMetadata(this.metadata);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* Set file-level synopsis calculation function
|
|
4060
|
+
*/
|
|
4061
|
+
setFileSynopsisFunction(fn) {
|
|
4062
|
+
this.fileSynopsisFunction = fn;
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Set version-level synopsis calculation function
|
|
4066
|
+
*/
|
|
4067
|
+
setVersionSynopsisFunction(fn) {
|
|
4068
|
+
this.versionSynopsisFunction = fn;
|
|
4069
|
+
}
|
|
4070
|
+
/**
|
|
4071
|
+
* Get current version name
|
|
4072
|
+
*/
|
|
4073
|
+
getCurrentVersion() {
|
|
4074
|
+
return this.currentVersion;
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Get current metadata
|
|
4078
|
+
*/
|
|
4079
|
+
getMetadata() {
|
|
4080
|
+
return { ...this.metadata };
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4083
|
+
* Find data by custom metadata fields
|
|
4084
|
+
* Searches through all versions and files to find entries matching the search criteria
|
|
4085
|
+
*
|
|
4086
|
+
* @param searchCriteria - Object with field names and values to search for (e.g., { ListingKey: "123", id: "456" })
|
|
4087
|
+
* @returns Array of found entries with their file paths and metadata
|
|
4088
|
+
*/
|
|
4089
|
+
async findData(searchCriteria) {
|
|
4090
|
+
const results = [];
|
|
4091
|
+
if (!this.versioned) {
|
|
4092
|
+
await this.prepare({ read: true });
|
|
4093
|
+
const metadata = this.getMetadata();
|
|
4094
|
+
for (const fileEntry of metadata.files) {
|
|
4095
|
+
const matches = Object.keys(searchCriteria).every((key) => {
|
|
4096
|
+
return fileEntry[key] === searchCriteria[key];
|
|
4097
|
+
});
|
|
4098
|
+
if (matches) {
|
|
4099
|
+
const destPath = this.getDestinationPath();
|
|
4100
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4101
|
+
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4102
|
+
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4103
|
+
results.push({
|
|
4104
|
+
filePath,
|
|
4105
|
+
fileName: fileEntry.fileName,
|
|
4106
|
+
version: null,
|
|
4107
|
+
metadata: fileEntry,
|
|
4108
|
+
data
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
} else {
|
|
4113
|
+
const versions = await this.getVersions();
|
|
4114
|
+
for (const version of versions) {
|
|
4115
|
+
await this.prepare({ read: true, version });
|
|
4116
|
+
const metadata = this.getMetadata();
|
|
4117
|
+
for (const fileEntry of metadata.files) {
|
|
4118
|
+
const matches = Object.keys(searchCriteria).every((key) => {
|
|
4119
|
+
return fileEntry[key] === searchCriteria[key];
|
|
4120
|
+
});
|
|
4121
|
+
if (matches) {
|
|
4122
|
+
const destPath = this.getDestinationPath(version);
|
|
4123
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4124
|
+
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4125
|
+
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4126
|
+
results.push({
|
|
4127
|
+
filePath,
|
|
4128
|
+
fileName: fileEntry.fileName,
|
|
4129
|
+
version,
|
|
4130
|
+
metadata: fileEntry,
|
|
4131
|
+
data
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
return results;
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
|
|
4141
|
+
// src/tasks/taskLogs.ts
|
|
4142
|
+
function getLogsState(context) {
|
|
4143
|
+
const holder = context;
|
|
4144
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4145
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4146
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4147
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4148
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4149
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4150
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4151
|
+
const errorDb = new FileDatabase({
|
|
4152
|
+
basePath,
|
|
4153
|
+
namespace,
|
|
4154
|
+
tableName: errorTableName,
|
|
4155
|
+
versioned: true,
|
|
4156
|
+
useMetadata: true,
|
|
4157
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4158
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4159
|
+
logger: holder.logger
|
|
4160
|
+
});
|
|
4161
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4162
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4163
|
+
if (!enabled) {
|
|
4164
|
+
const disabledState = {
|
|
4165
|
+
db: null,
|
|
4166
|
+
errorDb,
|
|
4167
|
+
queue: Promise.resolve(),
|
|
4168
|
+
initialized: true,
|
|
4169
|
+
errorInitialized: false
|
|
4170
|
+
};
|
|
4171
|
+
holder.__tasksLogsState = disabledState;
|
|
4172
|
+
return disabledState;
|
|
4173
|
+
}
|
|
4174
|
+
const db = new FileDatabase({
|
|
4175
|
+
basePath,
|
|
4176
|
+
namespace,
|
|
4177
|
+
tableName,
|
|
4178
|
+
versioned: true,
|
|
4179
|
+
useMetadata: true,
|
|
4180
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4181
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4182
|
+
logger: holder.logger
|
|
4183
|
+
});
|
|
4184
|
+
const state = {
|
|
4185
|
+
db,
|
|
4186
|
+
errorDb,
|
|
4187
|
+
queue: Promise.resolve(),
|
|
4188
|
+
initialized: false,
|
|
4189
|
+
errorInitialized: false
|
|
4190
|
+
};
|
|
4191
|
+
holder.__tasksLogsState = state;
|
|
4192
|
+
return state;
|
|
4193
|
+
}
|
|
4194
|
+
function isErrorPayload(payload) {
|
|
4195
|
+
if (!payload) return false;
|
|
4196
|
+
if (typeof payload === "object") {
|
|
4197
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4198
|
+
if (level === "error" || level === "fatal") return true;
|
|
4199
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4200
|
+
return false;
|
|
4201
|
+
}
|
|
4202
|
+
if (typeof payload === "string") {
|
|
4203
|
+
return /\berror\b/i.test(payload);
|
|
4204
|
+
}
|
|
4205
|
+
return false;
|
|
4206
|
+
}
|
|
4207
|
+
function buildLogRecord(task, payload) {
|
|
4208
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4209
|
+
return {
|
|
4210
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4211
|
+
opid: task.opid ?? null,
|
|
4212
|
+
taskId: task.id,
|
|
4213
|
+
taskName: task.task,
|
|
4214
|
+
target: task.target,
|
|
4215
|
+
source: typeof params.source === "string" ? params.source : null,
|
|
4216
|
+
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4217
|
+
payload
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
function appendTaskIpcLog(context, task, payload) {
|
|
4221
|
+
const state = getLogsState(context);
|
|
4222
|
+
if (!state.db && !state.errorDb) return;
|
|
4223
|
+
const record = buildLogRecord(task, payload);
|
|
4224
|
+
state.queue = state.queue.then(async () => {
|
|
4225
|
+
if (state.db) {
|
|
4226
|
+
await state.db.write([record], { forceNewVersion: !state.initialized });
|
|
4227
|
+
state.initialized = true;
|
|
4228
|
+
}
|
|
4229
|
+
if (state.errorDb && isErrorPayload(payload)) {
|
|
4230
|
+
await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
|
|
4231
|
+
state.errorInitialized = true;
|
|
4232
|
+
}
|
|
4233
|
+
}).catch((error) => {
|
|
4234
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
// src/tasks/time-matcher.ts
|
|
4239
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4240
|
+
function resolveAsterisks(field, range) {
|
|
4241
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4242
|
+
}
|
|
4243
|
+
function resolveRanges(field) {
|
|
4244
|
+
const regex = /(\d+)-(\d+)/;
|
|
4245
|
+
let current = field;
|
|
4246
|
+
while (true) {
|
|
4247
|
+
const match = regex.exec(current);
|
|
4248
|
+
if (!match) break;
|
|
4249
|
+
const raw = match[0];
|
|
4250
|
+
let first = Number(match[1]);
|
|
4251
|
+
let last = Number(match[2]);
|
|
4252
|
+
if (last < first) {
|
|
4253
|
+
[first, last] = [last, first];
|
|
4254
|
+
}
|
|
4255
|
+
const values = [];
|
|
4256
|
+
for (let i = first; i <= last; i += 1) {
|
|
4257
|
+
values.push(i);
|
|
4258
|
+
}
|
|
4259
|
+
current = current.replace(raw, values.join(","));
|
|
4260
|
+
}
|
|
4261
|
+
return current;
|
|
4262
|
+
}
|
|
4263
|
+
function resolveSteps(field) {
|
|
4264
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4265
|
+
if (!match) return field;
|
|
4266
|
+
const base = match[1];
|
|
4267
|
+
const step = Number(match[2]);
|
|
4268
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4269
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4270
|
+
}
|
|
4271
|
+
function convertPattern(pattern) {
|
|
4272
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4273
|
+
if (parts.length !== 6) {
|
|
4274
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4275
|
+
}
|
|
4276
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4277
|
+
}
|
|
4278
|
+
function fieldMatches(field, value) {
|
|
4279
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4280
|
+
return allowed.includes(value);
|
|
4281
|
+
}
|
|
4282
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4283
|
+
const parsed = convertPattern(pattern);
|
|
4284
|
+
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());
|
|
4285
|
+
}
|
|
4286
|
+
|
|
4287
|
+
// src/tasks/TaskMaster.ts
|
|
4288
|
+
var TaskMaster = class {
|
|
4289
|
+
context;
|
|
4290
|
+
task;
|
|
4291
|
+
constructor(context, task) {
|
|
4292
|
+
this.context = context;
|
|
4293
|
+
this.task = task;
|
|
4294
|
+
}
|
|
4295
|
+
cantRunReason() {
|
|
4296
|
+
return false;
|
|
4297
|
+
}
|
|
4298
|
+
requestStop(_allowanceMs) {
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
|
|
4302
|
+
// src/tasks/coreTasks/TaskPing.ts
|
|
4303
|
+
var TaskPing = class extends TaskMaster {
|
|
4304
|
+
async run() {
|
|
4305
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4306
|
+
return { success: true, results: "pong" };
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
4309
|
+
|
|
4310
|
+
// src/tasks/coreTasks/TaskSampleProcess.ts
|
|
4311
|
+
var TaskSampleProcess = class extends TaskMaster {
|
|
4312
|
+
stopRequested = false;
|
|
4313
|
+
stopAllowanceMs = 0;
|
|
4314
|
+
stopDecisionLogged = false;
|
|
4315
|
+
requestStop(allowanceMs) {
|
|
4316
|
+
this.stopRequested = true;
|
|
4317
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
4318
|
+
this.context.logger.warn?.(
|
|
4319
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
async run(reportProgress) {
|
|
4323
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
4324
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
4325
|
+
const nameRaw = this.task?.params?.name;
|
|
4326
|
+
const total = Number(totalRaw);
|
|
4327
|
+
const delay = Number(delayRaw);
|
|
4328
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
4329
|
+
const errors = [];
|
|
4330
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4331
|
+
errors.push('param "total" must be a positive integer');
|
|
4332
|
+
}
|
|
4333
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4334
|
+
errors.push('param "delay" must be an integer >= 0');
|
|
4335
|
+
}
|
|
4336
|
+
if (errors.length > 0) {
|
|
4337
|
+
return {
|
|
4338
|
+
success: false,
|
|
4339
|
+
results: {
|
|
4340
|
+
error: `Validation failed: ${errors.join(", ")}`,
|
|
4341
|
+
received: { total: totalRaw, delay: delayRaw, name: nameRaw }
|
|
4342
|
+
}
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
const startedAt = Date.now();
|
|
4346
|
+
for (let i = 1; i <= total; i += 1) {
|
|
4347
|
+
if (this.stopRequested) {
|
|
4348
|
+
const remainingMs = Math.max(0, (total - i + 1) * delay);
|
|
4349
|
+
if (remainingMs <= this.stopAllowanceMs) {
|
|
4350
|
+
if (!this.stopDecisionLogged) {
|
|
4351
|
+
this.stopDecisionLogged = true;
|
|
4352
|
+
this.context.logger.warn?.(
|
|
4353
|
+
`[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
|
|
4354
|
+
);
|
|
4355
|
+
}
|
|
4356
|
+
} else {
|
|
4357
|
+
this.context.logger.warn?.(
|
|
4358
|
+
`[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
|
|
4359
|
+
);
|
|
4360
|
+
return {
|
|
4361
|
+
success: false,
|
|
4362
|
+
results: {
|
|
4363
|
+
message: `Stopped before completion at iteration ${i}/${total}`,
|
|
4364
|
+
completed: i - 1,
|
|
4365
|
+
total,
|
|
4366
|
+
name,
|
|
4367
|
+
remainingMs,
|
|
4368
|
+
allowanceMs: this.stopAllowanceMs
|
|
4369
|
+
}
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
const elapsed = Date.now() - startedAt;
|
|
4374
|
+
const remaining = Math.max(0, (total - i) * delay);
|
|
4375
|
+
const progress = {
|
|
4376
|
+
name,
|
|
4377
|
+
count: i,
|
|
4378
|
+
total,
|
|
4379
|
+
elapsedMs: elapsed,
|
|
4380
|
+
remainingMs: remaining,
|
|
4381
|
+
status: `running ${name}: ${i}/${total}`
|
|
4382
|
+
};
|
|
4383
|
+
this.context.logger.progress("running", {
|
|
4384
|
+
prefix: name,
|
|
4385
|
+
count: i,
|
|
4386
|
+
total
|
|
4387
|
+
});
|
|
4388
|
+
await reportProgress(progress);
|
|
4389
|
+
await sleepMs(delay);
|
|
4390
|
+
}
|
|
4391
|
+
return {
|
|
4392
|
+
success: true,
|
|
4393
|
+
results: {
|
|
4394
|
+
message: `Completed ${total} iterations`,
|
|
4395
|
+
total,
|
|
4396
|
+
delay,
|
|
4397
|
+
name
|
|
4398
|
+
}
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
|
|
4403
|
+
// src/tasks/coreTasks/TaskShellCommand.ts
|
|
4404
|
+
import { spawn } from "child_process";
|
|
4405
|
+
function runShellCommand(command, cwd) {
|
|
4406
|
+
return new Promise((resolve2, reject) => {
|
|
4407
|
+
const child = spawn(command, {
|
|
4408
|
+
shell: true,
|
|
4409
|
+
cwd: cwd || process.cwd(),
|
|
4410
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4411
|
+
});
|
|
4412
|
+
let output = "";
|
|
4413
|
+
let stderr = "";
|
|
4414
|
+
child.stdout.on("data", (chunk) => {
|
|
4415
|
+
output += String(chunk);
|
|
4416
|
+
});
|
|
4417
|
+
child.stderr.on("data", (chunk) => {
|
|
4418
|
+
stderr += String(chunk);
|
|
4419
|
+
});
|
|
4420
|
+
child.on("error", (error) => {
|
|
4421
|
+
reject(error);
|
|
4422
|
+
});
|
|
4423
|
+
child.on("close", (exitCode, signal) => {
|
|
4424
|
+
resolve2({
|
|
4425
|
+
exitCode,
|
|
4426
|
+
output: output.trim(),
|
|
4427
|
+
stderr: stderr.trim(),
|
|
4428
|
+
signal
|
|
4429
|
+
});
|
|
4430
|
+
});
|
|
4431
|
+
});
|
|
4432
|
+
}
|
|
4433
|
+
var TaskShellCommand = class extends TaskMaster {
|
|
4434
|
+
async run() {
|
|
4435
|
+
const params = this.task?.params;
|
|
4436
|
+
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
4437
|
+
const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
|
|
4438
|
+
const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
|
|
4439
|
+
const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
|
|
4440
|
+
if (!command) {
|
|
4441
|
+
return {
|
|
4442
|
+
success: false,
|
|
4443
|
+
results: {
|
|
4444
|
+
error: 'Validation failed: param "command" must be a non-empty string',
|
|
4445
|
+
received: this.task?.params ?? null
|
|
4446
|
+
}
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
try {
|
|
4450
|
+
const result = await runShellCommand(command, cwd);
|
|
4451
|
+
const success = result.exitCode === 0;
|
|
4452
|
+
this.context.logger.info?.(
|
|
4453
|
+
`[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
|
|
4454
|
+
);
|
|
4455
|
+
return {
|
|
4456
|
+
success,
|
|
4457
|
+
results: {
|
|
4458
|
+
command,
|
|
4459
|
+
cwd: cwd ?? process.cwd(),
|
|
4460
|
+
output: result.output,
|
|
4461
|
+
stderr: result.stderr,
|
|
4462
|
+
exitCode: result.exitCode,
|
|
4463
|
+
signal: result.signal
|
|
4464
|
+
}
|
|
4465
|
+
};
|
|
4466
|
+
} catch (error) {
|
|
4467
|
+
return {
|
|
4468
|
+
success: false,
|
|
4469
|
+
results: {
|
|
4470
|
+
command,
|
|
4471
|
+
cwd: cwd ?? process.cwd(),
|
|
4472
|
+
output: "",
|
|
4473
|
+
stderr: "",
|
|
4474
|
+
exitCode: null,
|
|
4475
|
+
error: error?.message ?? String(error)
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
};
|
|
4481
|
+
|
|
4482
|
+
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
4483
|
+
import os from "os";
|
|
4484
|
+
import fs4 from "fs/promises";
|
|
4485
|
+
function toGb(valueBytes) {
|
|
4486
|
+
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4487
|
+
}
|
|
4488
|
+
function toMb(valueBytes) {
|
|
4489
|
+
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4490
|
+
}
|
|
4491
|
+
async function getDiskStats() {
|
|
4492
|
+
const stats = await fs4.statfs("/");
|
|
4493
|
+
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4494
|
+
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4495
|
+
const used = total - free;
|
|
4496
|
+
return {
|
|
4497
|
+
total: toGb(total),
|
|
4498
|
+
used: toGb(used),
|
|
4499
|
+
free: toGb(free)
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
var TaskSystemInfo = class extends TaskMaster {
|
|
4503
|
+
async run() {
|
|
4504
|
+
try {
|
|
4505
|
+
const totalMemory = os.totalmem();
|
|
4506
|
+
const freeMemory = os.freemem();
|
|
4507
|
+
const usedMemory = totalMemory - freeMemory;
|
|
4508
|
+
const cpus = os.cpus();
|
|
4509
|
+
const cpuUtilization = cpus.map((cpu) => {
|
|
4510
|
+
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4511
|
+
const usage = (total - cpu.times.idle) / total * 100;
|
|
4512
|
+
return Number(usage.toFixed(2));
|
|
4513
|
+
});
|
|
4514
|
+
const processMemory = process.memoryUsage();
|
|
4515
|
+
const disk = await getDiskStats();
|
|
4516
|
+
const results = {
|
|
4517
|
+
memory: {
|
|
4518
|
+
total: toGb(totalMemory),
|
|
4519
|
+
used: toGb(usedMemory),
|
|
4520
|
+
free: toGb(freeMemory)
|
|
4521
|
+
},
|
|
4522
|
+
processMemory: {
|
|
4523
|
+
rss: toMb(processMemory.rss),
|
|
4524
|
+
heapTotal: toMb(processMemory.heapTotal),
|
|
4525
|
+
heapUsed: toMb(processMemory.heapUsed),
|
|
4526
|
+
external: toMb(processMemory.external)
|
|
4527
|
+
},
|
|
4528
|
+
disk,
|
|
4529
|
+
cpu: {
|
|
4530
|
+
cores: cpuUtilization.length,
|
|
4531
|
+
utilization: cpuUtilization
|
|
4532
|
+
},
|
|
4533
|
+
runtime: {
|
|
4534
|
+
platform: os.platform(),
|
|
4535
|
+
arch: os.arch(),
|
|
4536
|
+
uptimeSec: os.uptime(),
|
|
4537
|
+
hostname: os.hostname()
|
|
4538
|
+
}
|
|
4539
|
+
};
|
|
4540
|
+
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
4541
|
+
return { success: true, results };
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
return {
|
|
4544
|
+
success: false,
|
|
4545
|
+
results: {
|
|
4546
|
+
error: "Can't collect system stats",
|
|
4547
|
+
message: error?.message ?? String(error)
|
|
4548
|
+
}
|
|
4549
|
+
};
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
};
|
|
4553
|
+
|
|
4554
|
+
// src/tasks/coreTasks/TaskSumAB.ts
|
|
4555
|
+
var TaskSumAB = class extends TaskMaster {
|
|
4556
|
+
async run() {
|
|
4557
|
+
const a = this.task?.params?.a;
|
|
4558
|
+
const b = this.task?.params?.b;
|
|
4559
|
+
if (typeof a !== "number" || Number.isNaN(a)) {
|
|
4560
|
+
return {
|
|
4561
|
+
success: false,
|
|
4562
|
+
results: {
|
|
4563
|
+
error: 'Validation failed: param "a" must be a valid number',
|
|
4564
|
+
received: { a, b }
|
|
4565
|
+
}
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
if (typeof b !== "number" || Number.isNaN(b)) {
|
|
4569
|
+
return {
|
|
4570
|
+
success: false,
|
|
4571
|
+
results: {
|
|
4572
|
+
error: 'Validation failed: param "b" must be a valid number',
|
|
4573
|
+
received: { a, b }
|
|
4574
|
+
}
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4577
|
+
const sum = a + b;
|
|
4578
|
+
this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
|
|
4579
|
+
return {
|
|
4580
|
+
success: true,
|
|
4581
|
+
results: { a, b, sum }
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
};
|
|
4585
|
+
|
|
4586
|
+
// src/tasks/coreTasks/TaskStopRunner.ts
|
|
4587
|
+
var TaskStopRunner = class extends TaskMaster {
|
|
4588
|
+
async run() {
|
|
4589
|
+
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4590
|
+
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
4591
|
+
return {
|
|
4592
|
+
success: true,
|
|
4593
|
+
results: {
|
|
4594
|
+
stopRunner: true,
|
|
4595
|
+
allowanceMs,
|
|
4596
|
+
message: "Runner stop requested"
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
};
|
|
4601
|
+
|
|
4602
|
+
// src/tasks/TasksRegistry.ts
|
|
4603
|
+
var TasksRegistry = class _TasksRegistry {
|
|
4604
|
+
map = {};
|
|
4605
|
+
constructor(initial) {
|
|
4606
|
+
if (initial) {
|
|
4607
|
+
this.addMany(initial);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
static withCoreTasks() {
|
|
4611
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
4612
|
+
}
|
|
4613
|
+
add(taskName, taskClass) {
|
|
4614
|
+
this.map[taskName] = taskClass;
|
|
4615
|
+
return this;
|
|
4616
|
+
}
|
|
4617
|
+
addMany(entries) {
|
|
4618
|
+
for (const [name, klass] of Object.entries(entries)) {
|
|
4619
|
+
this.add(name, klass);
|
|
4620
|
+
}
|
|
4621
|
+
return this;
|
|
4622
|
+
}
|
|
4623
|
+
get(taskName) {
|
|
4624
|
+
return this.map[taskName];
|
|
4625
|
+
}
|
|
4626
|
+
listSupportedTasks() {
|
|
4627
|
+
return Object.keys(this.map).sort();
|
|
4628
|
+
}
|
|
4629
|
+
toObject() {
|
|
4630
|
+
return { ...this.map };
|
|
4631
|
+
}
|
|
4632
|
+
};
|
|
4633
|
+
|
|
4634
|
+
// src/tasks/taskScriptRunner.ts
|
|
4635
|
+
import { spawn as spawn2 } from "child_process";
|
|
4636
|
+
|
|
4637
|
+
// src/tasks/index.ts
|
|
4638
|
+
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4639
|
+
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4640
|
+
function getDb2(context) {
|
|
4641
|
+
const db = context.db;
|
|
4642
|
+
if (!db) {
|
|
4643
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4644
|
+
}
|
|
4645
|
+
return db;
|
|
4646
|
+
}
|
|
4647
|
+
function normalizeRegistry(registry) {
|
|
4648
|
+
if (!registry) return defaultTasksRegistry;
|
|
4649
|
+
if (registry instanceof TasksRegistry) return registry;
|
|
4650
|
+
return new TasksRegistry().addMany(registry);
|
|
4651
|
+
}
|
|
4652
|
+
function normalizeAllowedTasks(value) {
|
|
4653
|
+
if (!value) return void 0;
|
|
4654
|
+
if (Array.isArray(value)) {
|
|
4655
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4656
|
+
return out2.length ? out2 : void 0;
|
|
4657
|
+
}
|
|
4658
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4659
|
+
return out.length ? out : void 0;
|
|
4660
|
+
}
|
|
4661
|
+
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4662
|
+
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4663
|
+
for (const [, taskInstance] of runningTaskInstances) {
|
|
4664
|
+
if (typeof taskInstance.requestStop === "function") {
|
|
4665
|
+
try {
|
|
4666
|
+
await taskInstance.requestStop(allowanceMs);
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
context.logger.warn?.("[tasks] task requestStop failed:", error);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
context.emitter.emit("stop", allowanceMs);
|
|
4673
|
+
}
|
|
4674
|
+
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4675
|
+
const db = getDb2(context);
|
|
4676
|
+
const taskName = row.task;
|
|
4677
|
+
const TaskClass = registry.get(taskName);
|
|
4678
|
+
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4679
|
+
if (!TaskClass) {
|
|
4680
|
+
const err = { message: `Unknown task "${taskName}"` };
|
|
4681
|
+
await db(historyTable).insert({
|
|
4682
|
+
...rowForHistory,
|
|
4683
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4684
|
+
success: false,
|
|
4685
|
+
params: toJsonColumn(row.params),
|
|
4686
|
+
results: toJsonColumn(err)
|
|
4687
|
+
});
|
|
4688
|
+
if (row.schedule) {
|
|
4689
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4690
|
+
started_at: null,
|
|
4691
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4692
|
+
success: false,
|
|
4693
|
+
results: toJsonColumn(err),
|
|
4694
|
+
past_due: null,
|
|
4695
|
+
paused_at: db.fn.now(),
|
|
4696
|
+
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4697
|
+
});
|
|
4698
|
+
} else {
|
|
4699
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4700
|
+
}
|
|
4701
|
+
return { stopRunnerRequested: false, stopAllowanceMs: 0 };
|
|
4702
|
+
}
|
|
4703
|
+
let success = false;
|
|
4704
|
+
let results = null;
|
|
4705
|
+
let taskInstance = null;
|
|
4706
|
+
try {
|
|
4707
|
+
taskInstance = new TaskClass(context, row);
|
|
4708
|
+
runningTaskInstances.set(row.id, taskInstance);
|
|
4709
|
+
const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
|
|
4710
|
+
success = !!runResult?.success;
|
|
4711
|
+
results = runResult?.results ?? null;
|
|
4712
|
+
} catch (error) {
|
|
4713
|
+
success = false;
|
|
4714
|
+
results = {
|
|
4715
|
+
message: error?.message ?? String(error),
|
|
4716
|
+
name: error?.name ?? "Error",
|
|
4717
|
+
stack: error?.stack ?? null
|
|
4718
|
+
};
|
|
4719
|
+
} finally {
|
|
4720
|
+
runningTaskInstances.delete(row.id);
|
|
4721
|
+
}
|
|
4722
|
+
await db(historyTable).insert({
|
|
4723
|
+
...rowForHistory,
|
|
4724
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4725
|
+
success,
|
|
4726
|
+
params: toJsonColumn(row.params),
|
|
4727
|
+
results: toJsonColumn(results)
|
|
4728
|
+
});
|
|
4729
|
+
if (!success) {
|
|
4730
|
+
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4731
|
+
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
4732
|
+
const fallbackRecoverCommand = [
|
|
4733
|
+
"npx",
|
|
4734
|
+
"tsx",
|
|
4735
|
+
"examples/tasks/recover-task.ts",
|
|
4736
|
+
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
4737
|
+
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
4738
|
+
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
4739
|
+
].join(" ");
|
|
4740
|
+
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4741
|
+
appendTaskIpcLog(context, row, {
|
|
4742
|
+
level: "error",
|
|
4743
|
+
message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4744
|
+
details: results
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4747
|
+
if (row.schedule) {
|
|
4748
|
+
if (success) {
|
|
4749
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4750
|
+
started_at: null,
|
|
4751
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4752
|
+
success,
|
|
4753
|
+
results: toJsonColumn(results),
|
|
4754
|
+
progress: null,
|
|
4755
|
+
past_due: null
|
|
4756
|
+
});
|
|
4757
|
+
} else {
|
|
4758
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4759
|
+
started_at: null,
|
|
4760
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4761
|
+
success,
|
|
4762
|
+
results: toJsonColumn(results),
|
|
4763
|
+
paused_at: db.fn.now(),
|
|
4764
|
+
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4765
|
+
past_due: null
|
|
4766
|
+
});
|
|
4767
|
+
}
|
|
4768
|
+
} else {
|
|
4769
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4770
|
+
}
|
|
4771
|
+
const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
|
|
4772
|
+
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4773
|
+
return { stopRunnerRequested, stopAllowanceMs };
|
|
4774
|
+
}
|
|
4775
|
+
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
4776
|
+
const db = getDb2(context);
|
|
4777
|
+
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);
|
|
4778
|
+
if (taskNames && taskNames.length > 0) {
|
|
4779
|
+
query = query.whereIn("task", taskNames);
|
|
4780
|
+
}
|
|
4781
|
+
const candidates = await query;
|
|
4782
|
+
for (const row of candidates) {
|
|
4783
|
+
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
const TaskClass = registry.get(row.task);
|
|
4787
|
+
if (TaskClass) {
|
|
4788
|
+
const taskInstance = new TaskClass(context, row);
|
|
4789
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
4790
|
+
if (reason) {
|
|
4791
|
+
if (!row.past_due) {
|
|
4792
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4793
|
+
past_due: db.fn.now(),
|
|
4794
|
+
progress: String(reason)
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
|
|
4801
|
+
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
4802
|
+
if (claimed) return claimed;
|
|
4803
|
+
}
|
|
4804
|
+
return null;
|
|
4805
|
+
}
|
|
4806
|
+
async function runTasksLoop(context, options) {
|
|
4807
|
+
const queue = options.queue ?? "tasks";
|
|
4808
|
+
const target = options.target;
|
|
4809
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
4810
|
+
const maxParallel = options.maxParallel ?? 1;
|
|
4811
|
+
const scanLimit = options.scanLimit ?? 100;
|
|
4812
|
+
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4813
|
+
const registry = normalizeRegistry(options.registry);
|
|
4814
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4815
|
+
if (!target) throw new Error("runTasksLoop: target is required");
|
|
4816
|
+
const runningPromises = /* @__PURE__ */ new Set();
|
|
4817
|
+
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
4818
|
+
let runningStopControlPromise = null;
|
|
4819
|
+
let stopRequested = false;
|
|
4820
|
+
let stopAllowanceMs = 5e3;
|
|
4821
|
+
context.__tasksRunnerStop = false;
|
|
4822
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
4823
|
+
if (!runningStopControlPromise) {
|
|
4824
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
4825
|
+
context,
|
|
4826
|
+
tasksTable,
|
|
4827
|
+
target,
|
|
4828
|
+
registry,
|
|
4829
|
+
10,
|
|
4830
|
+
["stopRunner", "stop"]
|
|
4831
|
+
);
|
|
4832
|
+
if (claimedStopTask) {
|
|
4833
|
+
runningStopControlPromise = executeClaimedTask(
|
|
4834
|
+
context,
|
|
4835
|
+
tasksTable,
|
|
4836
|
+
historyTable,
|
|
4837
|
+
claimedStopTask,
|
|
4838
|
+
registry,
|
|
4839
|
+
runningTaskInstances
|
|
4840
|
+
).then(async (outcome) => {
|
|
4841
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4842
|
+
stopRequested = true;
|
|
4843
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4844
|
+
context.__tasksRunnerStop = true;
|
|
4845
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4846
|
+
}
|
|
4847
|
+
}).finally(() => {
|
|
4848
|
+
runningStopControlPromise = null;
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
while (runningPromises.size < maxParallel) {
|
|
4853
|
+
const claimed = await claimNextRunnableTask(
|
|
4854
|
+
context,
|
|
4855
|
+
tasksTable,
|
|
4856
|
+
target,
|
|
4857
|
+
registry,
|
|
4858
|
+
scanLimit,
|
|
4859
|
+
allowedTasks
|
|
4860
|
+
);
|
|
4861
|
+
if (!claimed) break;
|
|
4862
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4863
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4864
|
+
stopRequested = true;
|
|
4865
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4866
|
+
context.__tasksRunnerStop = true;
|
|
4867
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4868
|
+
}
|
|
4869
|
+
}).finally(() => {
|
|
4870
|
+
runningPromises.delete(p);
|
|
4871
|
+
});
|
|
4872
|
+
runningPromises.add(p);
|
|
4873
|
+
}
|
|
4874
|
+
await sleepMs(pollMs);
|
|
4875
|
+
}
|
|
4876
|
+
if (context.isStop() && !stopRequested) {
|
|
4877
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
4878
|
+
}
|
|
4879
|
+
if (runningPromises.size > 0) {
|
|
4880
|
+
if (stopRequested) {
|
|
4881
|
+
await Promise.race([
|
|
4882
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
4883
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
4884
|
+
context.logger.warn?.(
|
|
4885
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
4886
|
+
);
|
|
4887
|
+
})
|
|
4888
|
+
]);
|
|
4889
|
+
} else {
|
|
4890
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
var TasksManager = class _TasksManager {
|
|
4895
|
+
context;
|
|
4896
|
+
queue;
|
|
4897
|
+
target;
|
|
4898
|
+
recreateTaskTables;
|
|
4899
|
+
pollMs;
|
|
4900
|
+
maxParallel;
|
|
4901
|
+
scanLimit;
|
|
4902
|
+
allowedTasks;
|
|
4903
|
+
registry;
|
|
4904
|
+
constructor(context, options = {}) {
|
|
4905
|
+
this.context = context;
|
|
4906
|
+
this.queue = options.queue ?? "tasks";
|
|
4907
|
+
this.target = options.target ?? "localRunner";
|
|
4908
|
+
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
4909
|
+
this.pollMs = options.pollMs ?? 1e3;
|
|
4910
|
+
this.maxParallel = options.maxParallel ?? 1;
|
|
4911
|
+
this.scanLimit = options.scanLimit ?? 100;
|
|
4912
|
+
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4913
|
+
this.registry = normalizeRegistry(options.registry);
|
|
4914
|
+
}
|
|
4915
|
+
static init(context, options = {}) {
|
|
4916
|
+
const defs2 = {
|
|
4917
|
+
table: "string default tasks",
|
|
4918
|
+
target: "string default localRunner",
|
|
4919
|
+
recreateTaskTables: "boolean default false",
|
|
4920
|
+
pollMs: "number default 1000",
|
|
4921
|
+
maxParallel: "number default 1",
|
|
4922
|
+
scanLimit: "number default 100",
|
|
4923
|
+
allowedTasks: "string"
|
|
4924
|
+
};
|
|
4925
|
+
const discovered = context.params.getAllForModule(defs2);
|
|
4926
|
+
const resolved = {
|
|
4927
|
+
queue: discovered.table,
|
|
4928
|
+
target: discovered.target,
|
|
4929
|
+
recreateTaskTables: discovered.recreateTaskTables,
|
|
4930
|
+
pollMs: discovered.pollMs,
|
|
4931
|
+
maxParallel: discovered.maxParallel,
|
|
4932
|
+
scanLimit: discovered.scanLimit,
|
|
4933
|
+
allowedTasks: discovered.allowedTasks,
|
|
4934
|
+
...options
|
|
4935
|
+
};
|
|
4936
|
+
return new _TasksManager(context, resolved);
|
|
4937
|
+
}
|
|
4938
|
+
async ensureTaskTables(options = {}) {
|
|
4939
|
+
await ensureTaskTables(this.context, {
|
|
4940
|
+
queue: this.queue,
|
|
4941
|
+
recreate: options.recreate ?? this.recreateTaskTables
|
|
4942
|
+
});
|
|
4943
|
+
}
|
|
4944
|
+
async runTasksLoop(options = {}) {
|
|
4945
|
+
await runTasksLoop(this.context, {
|
|
4946
|
+
queue: options.queue ?? this.queue,
|
|
4947
|
+
target: options.target ?? this.target,
|
|
4948
|
+
pollMs: options.pollMs ?? this.pollMs,
|
|
4949
|
+
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
4950
|
+
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
4951
|
+
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
4952
|
+
registry: options.registry ?? this.registry
|
|
4953
|
+
});
|
|
4954
|
+
}
|
|
4955
|
+
};
|
|
4956
|
+
|
|
4957
|
+
// src/scripts/cli-runner.ts
|
|
4958
|
+
var defs = {
|
|
4959
|
+
dbName: "string default local",
|
|
4960
|
+
tasksModule: "string"
|
|
4961
|
+
};
|
|
4962
|
+
async function loadTasksModule(modulePath) {
|
|
4963
|
+
const absolute = path4.isAbsolute(modulePath) ? modulePath : path4.resolve(process.cwd(), modulePath);
|
|
4964
|
+
const imported = await import(pathToFileURL(absolute).href);
|
|
4965
|
+
if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
|
|
4966
|
+
throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);
|
|
4967
|
+
}
|
|
4968
|
+
return imported.tasksRegistry;
|
|
4969
|
+
}
|
|
4970
|
+
var flow = async (context) => {
|
|
4971
|
+
const {
|
|
4972
|
+
dbName,
|
|
4973
|
+
tasksModule
|
|
4974
|
+
} = context.params.getAll(defs);
|
|
4975
|
+
const db = await dbInit(context, dbName);
|
|
4976
|
+
context.db = db;
|
|
4977
|
+
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
4978
|
+
if (tasksModule) {
|
|
4979
|
+
const externalRegistry = await loadTasksModule(tasksModule);
|
|
4980
|
+
registry.addMany(externalRegistry);
|
|
4981
|
+
}
|
|
4982
|
+
const tasksManager = TasksManager.init(context, {
|
|
4983
|
+
registry
|
|
4984
|
+
});
|
|
4985
|
+
await tasksManager.ensureTaskTables();
|
|
4986
|
+
await tasksManager.runTasksLoop();
|
|
4987
|
+
};
|
|
4988
|
+
void init(flow);
|
|
4989
|
+
//# sourceMappingURL=cli-runner.js.map
|