@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
package/dist/index.cjs
CHANGED
|
@@ -1107,8 +1107,18 @@ __export(src_exports, {
|
|
|
1107
1107
|
ScreenFooter: () => ScreenFooter,
|
|
1108
1108
|
ScreenRow: () => ScreenRow,
|
|
1109
1109
|
ScreenTitle: () => ScreenTitle,
|
|
1110
|
+
TaskMaster: () => TaskMaster,
|
|
1111
|
+
TaskPing: () => TaskPing,
|
|
1112
|
+
TaskSampleProcess: () => TaskSampleProcess,
|
|
1113
|
+
TaskShellCommand: () => TaskShellCommand,
|
|
1114
|
+
TaskStopRunner: () => TaskStopRunner,
|
|
1115
|
+
TaskSumAB: () => TaskSumAB,
|
|
1116
|
+
TaskSystemInfo: () => TaskSystemInfo,
|
|
1117
|
+
TasksManager: () => TasksManager,
|
|
1118
|
+
TasksRegistry: () => TasksRegistry,
|
|
1110
1119
|
Text: () => import_ink5.Text,
|
|
1111
1120
|
TextBlock: () => TextBlock,
|
|
1121
|
+
appendTaskIpcLog: () => appendTaskIpcLog,
|
|
1112
1122
|
buildBreadcrumb: () => buildBreadcrumb,
|
|
1113
1123
|
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
1114
1124
|
buildFooter: () => buildFooter,
|
|
@@ -1116,8 +1126,11 @@ __export(src_exports, {
|
|
|
1116
1126
|
dbFindAndConnect: () => dbFindAndConnect,
|
|
1117
1127
|
dbInit: () => dbInit,
|
|
1118
1128
|
defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
|
|
1129
|
+
defaultTasksRegistry: () => defaultTasksRegistry,
|
|
1119
1130
|
defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
|
|
1120
|
-
|
|
1131
|
+
enqueueStopTask: () => enqueueStopTask,
|
|
1132
|
+
enqueueTask: () => enqueueTask,
|
|
1133
|
+
ensureTaskTables: () => ensureTaskTables,
|
|
1121
1134
|
getArgsInstance: () => getArgsInstance,
|
|
1122
1135
|
h: () => import_react5.createElement,
|
|
1123
1136
|
joiEdateType: () => joiEdateType,
|
|
@@ -1126,6 +1139,9 @@ __export(src_exports, {
|
|
|
1126
1139
|
listTables: () => listTables,
|
|
1127
1140
|
load: () => load,
|
|
1128
1141
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1142
|
+
queueToTableNames: () => queueToTableNames,
|
|
1143
|
+
runNodeTaskScript: () => runNodeTaskScript,
|
|
1144
|
+
runTasksLoop: () => runTasksLoop,
|
|
1129
1145
|
setupContext: () => setupContext,
|
|
1130
1146
|
showListScreen: () => showListScreen,
|
|
1131
1147
|
showMenuScreen: () => showMenuScreen,
|
|
@@ -1133,12 +1149,14 @@ __export(src_exports, {
|
|
|
1133
1149
|
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
1134
1150
|
showScreen: () => showScreen,
|
|
1135
1151
|
showWordGridScreen: () => showWordGridScreen,
|
|
1152
|
+
updateTaskProgress: () => updateTaskProgress,
|
|
1136
1153
|
useCallback: () => import_react5.useCallback,
|
|
1137
1154
|
useEffect: () => import_react5.useEffect,
|
|
1138
1155
|
useInput: () => import_ink5.useInput,
|
|
1139
1156
|
useMemo: () => import_react5.useMemo,
|
|
1140
1157
|
useRef: () => import_react5.useRef,
|
|
1141
|
-
useState: () => import_react5.useState
|
|
1158
|
+
useState: () => import_react5.useState,
|
|
1159
|
+
waitForTaskResult: () => waitForTaskResult
|
|
1142
1160
|
});
|
|
1143
1161
|
module.exports = __toCommonJS(src_exports);
|
|
1144
1162
|
|
|
@@ -1160,20 +1178,31 @@ var Args = class _Args {
|
|
|
1160
1178
|
configValues = {};
|
|
1161
1179
|
configsLoaded = [];
|
|
1162
1180
|
env = "local";
|
|
1163
|
-
constructor(
|
|
1181
|
+
constructor(contextOrConfig = {}, config2) {
|
|
1182
|
+
const hasContext = config2 !== void 0;
|
|
1183
|
+
const configToUse = hasContext ? config2 ?? {} : contextOrConfig ?? {};
|
|
1184
|
+
const context = hasContext ? contextOrConfig : void 0;
|
|
1164
1185
|
this.aliases = {};
|
|
1165
1186
|
this.overrides = {};
|
|
1166
1187
|
this.defaults = {};
|
|
1167
1188
|
this.prefixes = ["not", "no"];
|
|
1168
|
-
if (Object.keys(
|
|
1169
|
-
this.configure(
|
|
1189
|
+
if (Object.keys(configToUse).length > 0) {
|
|
1190
|
+
this.configure(configToUse);
|
|
1170
1191
|
}
|
|
1171
|
-
const args =
|
|
1192
|
+
const args = configToUse.args || process.argv.slice(2);
|
|
1172
1193
|
this.parseArgs(args);
|
|
1173
1194
|
this.env = this.get("env")?.toLowerCase() || "local";
|
|
1174
1195
|
this.loadDotEnv();
|
|
1175
1196
|
this.loadConfigFiles();
|
|
1176
1197
|
this.checkConflicts();
|
|
1198
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1199
|
+
context.registerCleanup((ctx) => {
|
|
1200
|
+
const unusedArgs = ctx.args.getUnused();
|
|
1201
|
+
if (unusedArgs.length > 0) {
|
|
1202
|
+
ctx.logger.warn("Unused CLI args:", unusedArgs.join(", "));
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1177
1206
|
}
|
|
1178
1207
|
/**
|
|
1179
1208
|
* Configure Args options
|
|
@@ -1195,12 +1224,15 @@ var Args = class _Args {
|
|
|
1195
1224
|
}
|
|
1196
1225
|
}
|
|
1197
1226
|
/**
|
|
1198
|
-
* Initialize Args instance
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1227
|
+
* Initialize Args instance.
|
|
1228
|
+
* Args.init(context, config) when used from init/setup: context has registerCleanup, Args registers unused-args cleanup.
|
|
1229
|
+
* Args.init(config) for standalone use (no cleanup).
|
|
1201
1230
|
*/
|
|
1202
|
-
static init(config2
|
|
1203
|
-
|
|
1231
|
+
static init(contextOrConfig, config2) {
|
|
1232
|
+
if (config2 !== void 0) {
|
|
1233
|
+
return new _Args(contextOrConfig, config2);
|
|
1234
|
+
}
|
|
1235
|
+
return new _Args(contextOrConfig ?? {});
|
|
1204
1236
|
}
|
|
1205
1237
|
/**
|
|
1206
1238
|
* Parse command line arguments
|
|
@@ -1380,6 +1412,32 @@ var Args = class _Args {
|
|
|
1380
1412
|
}
|
|
1381
1413
|
return void 0;
|
|
1382
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Return which layer provided the value for get(key): overrides, cli, config, env, or default.
|
|
1417
|
+
* Does not add key to usedKeys. Use after get(key) when you need the origin.
|
|
1418
|
+
*/
|
|
1419
|
+
getSource(key) {
|
|
1420
|
+
const resolvedKey = this.aliases[key] || key;
|
|
1421
|
+
const lcKey = resolvedKey.toLowerCase();
|
|
1422
|
+
const overrideKey = Object.keys(this.overrides).find((k) => k.toLowerCase() === lcKey);
|
|
1423
|
+
if (overrideKey !== void 0) return "overrides";
|
|
1424
|
+
const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
|
|
1425
|
+
if (this.env && this.args[lcKeyWithEnv] !== void 0) return "cli";
|
|
1426
|
+
if (this.args[lcKey] !== void 0) return "cli";
|
|
1427
|
+
const configKey = Object.keys(this.configValues).find((k) => k.toLowerCase() === lcKey);
|
|
1428
|
+
if (configKey !== void 0) return "config";
|
|
1429
|
+
const envKey = this.toEnvKey(resolvedKey);
|
|
1430
|
+
const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
|
|
1431
|
+
const envSpecificKey = Object.keys(process.env).find((k) => this.env && k.toUpperCase() === envKeyWithEnv);
|
|
1432
|
+
const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
|
|
1433
|
+
const envKeyAlt = envKey.replace(/_([0-9])/g, "$1");
|
|
1434
|
+
const envKeyAltFound = !envKeyFound ? Object.keys(process.env).find((k) => k.toUpperCase() === envKeyAlt) : null;
|
|
1435
|
+
if (envSpecificKey || envKeyFound || envKeyAltFound) return "env";
|
|
1436
|
+
const defaultKey = Object.keys(this.defaults).find((k) => k.toLowerCase() === lcKey);
|
|
1437
|
+
if (defaultKey !== void 0) return "default";
|
|
1438
|
+
if (lcKey === "env" && process.env.NODE_ENV !== void 0) return "env";
|
|
1439
|
+
return void 0;
|
|
1440
|
+
}
|
|
1383
1441
|
/**
|
|
1384
1442
|
* Set a value (for testing/internal use)
|
|
1385
1443
|
*/
|
|
@@ -1605,6 +1663,12 @@ var ParamError = class extends FrameworkError {
|
|
|
1605
1663
|
this.name = "ParamError";
|
|
1606
1664
|
}
|
|
1607
1665
|
};
|
|
1666
|
+
var FileDatabaseError = class extends FrameworkError {
|
|
1667
|
+
constructor(message) {
|
|
1668
|
+
super(message);
|
|
1669
|
+
this.name = "FileDatabaseError";
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1608
1672
|
|
|
1609
1673
|
// src/params/custom-types.ts
|
|
1610
1674
|
var joiEdateType = (value, helpers) => {
|
|
@@ -1730,17 +1794,53 @@ var Params = class _Params {
|
|
|
1730
1794
|
context;
|
|
1731
1795
|
// Partial context during initialization
|
|
1732
1796
|
params = {};
|
|
1797
|
+
paramSources = {};
|
|
1733
1798
|
definitions = {};
|
|
1734
1799
|
args;
|
|
1735
1800
|
paramSetters = [];
|
|
1736
1801
|
paramGetters = [];
|
|
1737
1802
|
trackedParams = [];
|
|
1803
|
+
_currentModule = "script";
|
|
1804
|
+
/** Resolved early in constructor so cleanup does not read params lazily */
|
|
1805
|
+
_showUsedParams = false;
|
|
1738
1806
|
constructor(context, options = {}) {
|
|
1739
1807
|
this.context = context;
|
|
1740
1808
|
this.args = context.args;
|
|
1741
1809
|
if (Object.keys(options).length > 0) {
|
|
1742
1810
|
this.configure(options);
|
|
1743
1811
|
}
|
|
1812
|
+
this._showUsedParams = this.get("showUsedParams", "boolean default false");
|
|
1813
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1814
|
+
context.registerCleanup((ctx) => {
|
|
1815
|
+
if (!ctx.params.getShowUsedParams()) return;
|
|
1816
|
+
const byModule = ctx.params.getFiguredByModule();
|
|
1817
|
+
const modules = Object.keys(byModule).sort();
|
|
1818
|
+
if (modules.length === 0) return;
|
|
1819
|
+
const logger = ctx.logger;
|
|
1820
|
+
logger.debug("[Params]: list of used params:");
|
|
1821
|
+
if (typeof logger.highlight !== "function") {
|
|
1822
|
+
for (const mod of modules) {
|
|
1823
|
+
logger.debug(` [${mod}]`);
|
|
1824
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1825
|
+
logger.debug(` ${key}: ${JSON.stringify(entry.value)} (${entry.source})`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
for (const mod of modules) {
|
|
1831
|
+
logger.debug(` [${mod}]`);
|
|
1832
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1833
|
+
const valueStr = JSON.stringify(entry.value);
|
|
1834
|
+
const display = entry.source === "default" ? valueStr : logger.highlight(valueStr);
|
|
1835
|
+
logger.debug(` ${key}: ${display} (${entry.source})`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
/** Whether --showUsedParams was requested (resolved in constructor). */
|
|
1842
|
+
getShowUsedParams() {
|
|
1843
|
+
return this._showUsedParams;
|
|
1744
1844
|
}
|
|
1745
1845
|
/**
|
|
1746
1846
|
* Configure parameters
|
|
@@ -1759,14 +1859,15 @@ var Params = class _Params {
|
|
|
1759
1859
|
return new _Params(context, options || {});
|
|
1760
1860
|
}
|
|
1761
1861
|
/**
|
|
1762
|
-
* Track a parameter request for --stopAfter=init
|
|
1862
|
+
* Track a parameter request for --stopAfter=init and --showUsedParams
|
|
1763
1863
|
*/
|
|
1764
|
-
trackParam(key, definition, value, source) {
|
|
1864
|
+
trackParam(key, definition, value, source, moduleName) {
|
|
1765
1865
|
this.trackedParams.push({
|
|
1766
1866
|
key,
|
|
1767
1867
|
definition,
|
|
1768
1868
|
value,
|
|
1769
|
-
source
|
|
1869
|
+
source,
|
|
1870
|
+
module: moduleName ?? this._currentModule
|
|
1770
1871
|
});
|
|
1771
1872
|
}
|
|
1772
1873
|
/**
|
|
@@ -1776,7 +1877,7 @@ var Params = class _Params {
|
|
|
1776
1877
|
return [...this.trackedParams];
|
|
1777
1878
|
}
|
|
1778
1879
|
/**
|
|
1779
|
-
* Get all figured parameters as a record
|
|
1880
|
+
* Get all figured parameters as a record (flat, last occurrence per key)
|
|
1780
1881
|
* Returns all parameters that were collected during initialization,
|
|
1781
1882
|
* whether from CLI args, options, or defaults
|
|
1782
1883
|
*/
|
|
@@ -1790,6 +1891,19 @@ var Params = class _Params {
|
|
|
1790
1891
|
}
|
|
1791
1892
|
return result;
|
|
1792
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Get figured parameters grouped by module name.
|
|
1896
|
+
* Same param can appear in multiple modules (e.g. source, resource).
|
|
1897
|
+
*/
|
|
1898
|
+
getFiguredByModule() {
|
|
1899
|
+
const byModule = {};
|
|
1900
|
+
for (const param of this.trackedParams) {
|
|
1901
|
+
const mod = param.module;
|
|
1902
|
+
if (!byModule[mod]) byModule[mod] = {};
|
|
1903
|
+
byModule[mod][param.key] = { value: param.value, source: param.source };
|
|
1904
|
+
}
|
|
1905
|
+
return byModule;
|
|
1906
|
+
}
|
|
1793
1907
|
/**
|
|
1794
1908
|
* Clear tracked parameters
|
|
1795
1909
|
*/
|
|
@@ -1908,14 +2022,19 @@ var Params = class _Params {
|
|
|
1908
2022
|
source = "options";
|
|
1909
2023
|
} else if (valFromArgs !== void 0 && valFromArgs !== null) {
|
|
1910
2024
|
value = this.validate(key, valFromArgs, def);
|
|
1911
|
-
|
|
2025
|
+
const argsSource = this.args.getSource?.(key);
|
|
2026
|
+
if (argsSource === "overrides") source = "options";
|
|
2027
|
+
else if (argsSource === "cli" || argsSource === "env" || argsSource === "config") source = argsSource;
|
|
2028
|
+
else if (argsSource === "default") source = "default";
|
|
2029
|
+
else source = "cli";
|
|
1912
2030
|
} else if (valFromParams !== void 0 && valFromParams !== null) {
|
|
1913
2031
|
value = this.validate(key, valFromParams, def);
|
|
1914
|
-
source = "options";
|
|
2032
|
+
source = this.paramSources[key] ?? "options";
|
|
1915
2033
|
} else {
|
|
1916
2034
|
value = this.validate(key, void 0, def);
|
|
1917
2035
|
source = "default";
|
|
1918
2036
|
}
|
|
2037
|
+
this.paramSources[key] = source;
|
|
1919
2038
|
this.trackParam(key, definition || "string", value, source);
|
|
1920
2039
|
if (value !== void 0 && def.values && !def.values.includes(value)) {
|
|
1921
2040
|
throw new ParamError(`key ${key} should be one of ${def.values}`);
|
|
@@ -1936,19 +2055,63 @@ var Params = class _Params {
|
|
|
1936
2055
|
}
|
|
1937
2056
|
}
|
|
1938
2057
|
/**
|
|
1939
|
-
* Get all parameters from definitions
|
|
1940
|
-
* Processes
|
|
2058
|
+
* Get all parameters from definitions (main script).
|
|
2059
|
+
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
1941
2060
|
*/
|
|
1942
2061
|
getAll(defs) {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2062
|
+
return this.getAllForModule("script", defs);
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Get all parameters from definitions for a given module name.
|
|
2066
|
+
* Figured params are grouped by module when using --showUsedParams.
|
|
2067
|
+
* Processes parameters left-to-right to support cross-parameter references.
|
|
2068
|
+
* If moduleName is omitted, it is inferred from the caller's file path (directory name under src/).
|
|
2069
|
+
*/
|
|
2070
|
+
getAllForModule(moduleNameOrDefs, defs) {
|
|
2071
|
+
let moduleName;
|
|
2072
|
+
let definitions;
|
|
2073
|
+
if (defs !== void 0) {
|
|
2074
|
+
moduleName = moduleNameOrDefs;
|
|
2075
|
+
definitions = defs;
|
|
2076
|
+
} else {
|
|
2077
|
+
definitions = moduleNameOrDefs;
|
|
2078
|
+
moduleName = this._inferModuleNameFromStack();
|
|
2079
|
+
}
|
|
2080
|
+
const prev = this._currentModule;
|
|
2081
|
+
this._currentModule = moduleName;
|
|
2082
|
+
try {
|
|
2083
|
+
const res = {};
|
|
2084
|
+
for (const [k, def] of Object.entries(definitions)) {
|
|
2085
|
+
const value = this.get(k, def);
|
|
2086
|
+
res[k] = value;
|
|
2087
|
+
if (value !== void 0) {
|
|
2088
|
+
this.params[k] = value;
|
|
2089
|
+
}
|
|
1949
2090
|
}
|
|
2091
|
+
return res;
|
|
2092
|
+
} finally {
|
|
2093
|
+
this._currentModule = prev;
|
|
1950
2094
|
}
|
|
1951
|
-
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2098
|
+
*/
|
|
2099
|
+
_inferModuleNameFromStack() {
|
|
2100
|
+
const stack = new Error().stack;
|
|
2101
|
+
if (!stack) return "script";
|
|
2102
|
+
const lines = stack.split("\n");
|
|
2103
|
+
const paramsIndexPath = "params" + (typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/") + "index.";
|
|
2104
|
+
for (const line of lines) {
|
|
2105
|
+
const parenMatch = line.match(/\(([^)]+)\)/);
|
|
2106
|
+
if (!parenMatch) continue;
|
|
2107
|
+
const parts = parenMatch[1].split(":");
|
|
2108
|
+
if (parts.length < 3) continue;
|
|
2109
|
+
const path4 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2110
|
+
if (!path4 || path4.includes(paramsIndexPath)) continue;
|
|
2111
|
+
const srcMatch = path4.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2112
|
+
if (srcMatch) return srcMatch[1];
|
|
2113
|
+
}
|
|
2114
|
+
return "script";
|
|
1952
2115
|
}
|
|
1953
2116
|
/**
|
|
1954
2117
|
* Run all registered getters for a key
|
|
@@ -2170,13 +2333,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2170
2333
|
}
|
|
2171
2334
|
|
|
2172
2335
|
// src/filedatabase/index.ts
|
|
2173
|
-
var
|
|
2174
|
-
constructor(message) {
|
|
2175
|
-
super(message);
|
|
2176
|
-
this.name = "FileDatabaseError";
|
|
2177
|
-
}
|
|
2178
|
-
};
|
|
2179
|
-
var FileDatabase = class {
|
|
2336
|
+
var FileDatabase = class _FileDatabase {
|
|
2180
2337
|
basePath;
|
|
2181
2338
|
namespace;
|
|
2182
2339
|
tableName = null;
|
|
@@ -2213,18 +2370,8 @@ var FileDatabase = class {
|
|
|
2213
2370
|
maxVersions: "number default 5",
|
|
2214
2371
|
pageSize: "number default 5000"
|
|
2215
2372
|
};
|
|
2216
|
-
const
|
|
2217
|
-
config2 = {
|
|
2218
|
-
basePath: opts.basePath ?? paramsConfig.basePath,
|
|
2219
|
-
namespace: opts.namespace ?? paramsConfig.namespace,
|
|
2220
|
-
tableName: opts.tableName ?? paramsConfig.tableName ?? null,
|
|
2221
|
-
versioned: opts.versioned ?? true,
|
|
2222
|
-
maxVersions: opts.maxVersions ?? paramsConfig.maxVersions,
|
|
2223
|
-
pageSize: opts.pageSize ?? paramsConfig.pageSize,
|
|
2224
|
-
useMetadata: opts.useMetadata ?? true,
|
|
2225
|
-
freeSpaceThreshold: opts.freeSpaceThreshold ?? 100 * 1024 * 1024,
|
|
2226
|
-
logger: context.logger
|
|
2227
|
-
};
|
|
2373
|
+
const discovered = context.params.getAllForModule(defs);
|
|
2374
|
+
config2 = { ...discovered, ...opts, logger: context.logger };
|
|
2228
2375
|
} else {
|
|
2229
2376
|
config2 = contextOrConfig;
|
|
2230
2377
|
}
|
|
@@ -2242,6 +2389,13 @@ var FileDatabase = class {
|
|
|
2242
2389
|
this.logger = config2.logger || console;
|
|
2243
2390
|
this.metadata = this.getDefaultMetadata();
|
|
2244
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Initialize FileDatabase from context and options.
|
|
2394
|
+
* Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
|
|
2395
|
+
*/
|
|
2396
|
+
static init(context, options) {
|
|
2397
|
+
return new _FileDatabase(context, options ?? {});
|
|
2398
|
+
}
|
|
2245
2399
|
/**
|
|
2246
2400
|
* Get default metadata structure
|
|
2247
2401
|
*/
|
|
@@ -2309,7 +2463,7 @@ var FileDatabase = class {
|
|
|
2309
2463
|
const versions = await this.getVersions();
|
|
2310
2464
|
while (versions.length > this.maxVersions) {
|
|
2311
2465
|
const versionToDelete = import_path4.default.resolve(this.getDestinationPath(), versions.shift());
|
|
2312
|
-
this.logger.
|
|
2466
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
2313
2467
|
await import_fs4.default.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
2314
2468
|
}
|
|
2315
2469
|
return versionName;
|
|
@@ -2587,7 +2741,7 @@ var FileDatabase = class {
|
|
|
2587
2741
|
};
|
|
2588
2742
|
this.metadata.files.push(fileEntry);
|
|
2589
2743
|
this.lastFileData = null;
|
|
2590
|
-
this.logger.
|
|
2744
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
2591
2745
|
}
|
|
2592
2746
|
/**
|
|
2593
2747
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -2620,7 +2774,7 @@ var FileDatabase = class {
|
|
|
2620
2774
|
const filesBeforeCreate = this.metadata.files.length;
|
|
2621
2775
|
this.makeNewFile();
|
|
2622
2776
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
2623
|
-
this.logger.
|
|
2777
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
2624
2778
|
} else if (this.metadata.files.length === 0) {
|
|
2625
2779
|
this.makeNewFile();
|
|
2626
2780
|
}
|
|
@@ -2669,7 +2823,7 @@ var FileDatabase = class {
|
|
|
2669
2823
|
dataLeftOver = null;
|
|
2670
2824
|
}
|
|
2671
2825
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
2672
|
-
this.logger.
|
|
2826
|
+
this.logger.silly?.(
|
|
2673
2827
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
2674
2828
|
);
|
|
2675
2829
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -2724,7 +2878,7 @@ var FileDatabase = class {
|
|
|
2724
2878
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2725
2879
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
2726
2880
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
2727
|
-
this.logger.
|
|
2881
|
+
this.logger.silly?.(
|
|
2728
2882
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
2729
2883
|
);
|
|
2730
2884
|
}
|
|
@@ -2748,7 +2902,7 @@ var FileDatabase = class {
|
|
|
2748
2902
|
}
|
|
2749
2903
|
try {
|
|
2750
2904
|
await import_fs4.default.promises.writeFile(filePath, serializedData, "utf8");
|
|
2751
|
-
this.logger.
|
|
2905
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
2752
2906
|
} catch (error) {
|
|
2753
2907
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
2754
2908
|
}
|
|
@@ -2830,7 +2984,8 @@ var FileDatabase = class {
|
|
|
2830
2984
|
this.useMetadata = format.hasMetadata;
|
|
2831
2985
|
}
|
|
2832
2986
|
if (this.useMetadata) {
|
|
2833
|
-
const
|
|
2987
|
+
const destPath = this.getDestinationPath();
|
|
2988
|
+
const metadataPath = import_path4.default.join(destPath, "metadata.json");
|
|
2834
2989
|
if (import_fs4.default.existsSync(metadataPath)) {
|
|
2835
2990
|
try {
|
|
2836
2991
|
const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
|
|
@@ -2844,7 +2999,9 @@ var FileDatabase = class {
|
|
|
2844
2999
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2845
3000
|
}
|
|
2846
3001
|
} else {
|
|
2847
|
-
throw new FileDatabaseError(
|
|
3002
|
+
throw new FileDatabaseError(
|
|
3003
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
3004
|
+
);
|
|
2848
3005
|
}
|
|
2849
3006
|
} else {
|
|
2850
3007
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -2861,6 +3018,13 @@ var FileDatabase = class {
|
|
|
2861
3018
|
* Write data to the file database
|
|
2862
3019
|
*/
|
|
2863
3020
|
async write(data, options = {}) {
|
|
3021
|
+
if (options.filename) {
|
|
3022
|
+
const destPath2 = this.getDestinationPath();
|
|
3023
|
+
await ensurePath(destPath2);
|
|
3024
|
+
const filePath = import_path4.default.join(destPath2, options.filename);
|
|
3025
|
+
await this.safeWrite(filePath, data);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
2864
3028
|
if (options.forceNewVersion && !this.versioned) {
|
|
2865
3029
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2866
3030
|
}
|
|
@@ -2884,17 +3048,17 @@ var FileDatabase = class {
|
|
|
2884
3048
|
});
|
|
2885
3049
|
if (matches) {
|
|
2886
3050
|
targetFileIndex = i;
|
|
2887
|
-
this.logger.
|
|
3051
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2888
3052
|
break;
|
|
2889
3053
|
} else {
|
|
2890
|
-
this.logger.
|
|
3054
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2891
3055
|
}
|
|
2892
3056
|
}
|
|
2893
3057
|
if (targetFileIndex === null) {
|
|
2894
|
-
this.logger.
|
|
3058
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
2895
3059
|
}
|
|
2896
3060
|
} else {
|
|
2897
|
-
this.logger.
|
|
3061
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
2898
3062
|
}
|
|
2899
3063
|
if (targetFileIndex !== null) {
|
|
2900
3064
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -2923,7 +3087,17 @@ var FileDatabase = class {
|
|
|
2923
3087
|
* Read data from the file database
|
|
2924
3088
|
*/
|
|
2925
3089
|
async read(options = {}) {
|
|
2926
|
-
const { version, nextPage = false, pageSize } = options;
|
|
3090
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
3091
|
+
if (filename) {
|
|
3092
|
+
const destPath = this.getDestinationPath(version);
|
|
3093
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3094
|
+
try {
|
|
3095
|
+
const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
|
|
3096
|
+
return JSON.parse(rawData);
|
|
3097
|
+
} catch (error) {
|
|
3098
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
2927
3101
|
await this.prepare({ read: true, version });
|
|
2928
3102
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
2929
3103
|
if (isNonPaginatedData) {
|
|
@@ -3004,6 +3178,67 @@ var FileDatabase = class {
|
|
|
3004
3178
|
this.currentRecord = 0;
|
|
3005
3179
|
this.hasReadFirstPage = false;
|
|
3006
3180
|
}
|
|
3181
|
+
/**
|
|
3182
|
+
* List filenames in the table directory.
|
|
3183
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
3184
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
3185
|
+
*/
|
|
3186
|
+
async listFilenames() {
|
|
3187
|
+
const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3188
|
+
try {
|
|
3189
|
+
const entries = await import_fs4.default.promises.readdir(destPath, { withFileTypes: true });
|
|
3190
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
3191
|
+
} catch (err) {
|
|
3192
|
+
if (err?.code === "ENOENT") return [];
|
|
3193
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Remove a file from the table directory (catalog mode).
|
|
3198
|
+
* Use with listFilenames() to manage individual files.
|
|
3199
|
+
*/
|
|
3200
|
+
async removeFile(filename) {
|
|
3201
|
+
const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3202
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3203
|
+
try {
|
|
3204
|
+
await import_fs4.default.promises.unlink(filePath);
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
if (err?.code === "ENOENT") return;
|
|
3207
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
3212
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
3213
|
+
*/
|
|
3214
|
+
async removeFileEntry(filename) {
|
|
3215
|
+
if (this.versioned) {
|
|
3216
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
3217
|
+
}
|
|
3218
|
+
await this.prepare({ read: true });
|
|
3219
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
3220
|
+
if (idx === -1) {
|
|
3221
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
3222
|
+
}
|
|
3223
|
+
const entry = this.metadata.files[idx];
|
|
3224
|
+
const recordsCount = entry.recordsCount || 0;
|
|
3225
|
+
this.metadata.files.splice(idx, 1);
|
|
3226
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
3227
|
+
const destPath = this.getDestinationPath();
|
|
3228
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3229
|
+
try {
|
|
3230
|
+
await import_fs4.default.promises.unlink(filePath);
|
|
3231
|
+
} catch (err) {
|
|
3232
|
+
if (err?.code === "ENOENT") {
|
|
3233
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
3234
|
+
} else {
|
|
3235
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
if (this.useMetadata) {
|
|
3239
|
+
await this.saveVersionMetadata(this.metadata);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3007
3242
|
/**
|
|
3008
3243
|
* Set file-level synopsis calculation function
|
|
3009
3244
|
*/
|
|
@@ -3107,9 +3342,6 @@ function listSources(basePath) {
|
|
|
3107
3342
|
return [];
|
|
3108
3343
|
}
|
|
3109
3344
|
}
|
|
3110
|
-
function fileDatabaseInit(context, options = {}) {
|
|
3111
|
-
return new FileDatabase(context, options);
|
|
3112
|
-
}
|
|
3113
3345
|
|
|
3114
3346
|
// src/db/index.ts
|
|
3115
3347
|
var import_knex = __toESM(require("knex"), 1);
|
|
@@ -3408,6 +3640,13 @@ var Db = class {
|
|
|
3408
3640
|
isConnectedToDb() {
|
|
3409
3641
|
return this.isConnected && this.knexInstance !== null;
|
|
3410
3642
|
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
3645
|
+
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
3646
|
+
*/
|
|
3647
|
+
static async init(context, dbNameOrConnectionString) {
|
|
3648
|
+
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
3649
|
+
}
|
|
3411
3650
|
};
|
|
3412
3651
|
function capitalizeFirstLetter(str) {
|
|
3413
3652
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
@@ -3421,7 +3660,7 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3421
3660
|
acquireConnectionTimeout: "number default 10000",
|
|
3422
3661
|
sslRejectUnauthorized: "boolean default false"
|
|
3423
3662
|
};
|
|
3424
|
-
const paramsConfig = context.params.
|
|
3663
|
+
const paramsConfig = context.params.getAllForModule(defs);
|
|
3425
3664
|
const config2 = {
|
|
3426
3665
|
connectionString,
|
|
3427
3666
|
name: paramsConfig.name || name || "default",
|
|
@@ -3471,7 +3710,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
|
3471
3710
|
dbConnectionString: "string",
|
|
3472
3711
|
dbProfile: "boolean default false"
|
|
3473
3712
|
};
|
|
3474
|
-
const paramsConfig = context.params.
|
|
3713
|
+
const paramsConfig = context.params.getAllForModule(defs);
|
|
3475
3714
|
dbName = paramsConfig.dbName;
|
|
3476
3715
|
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3477
3716
|
dbProfile = paramsConfig.dbProfile;
|
|
@@ -3533,6 +3772,7 @@ var ALL_LEVELS = [
|
|
|
3533
3772
|
"response",
|
|
3534
3773
|
"progress"
|
|
3535
3774
|
];
|
|
3775
|
+
var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
|
|
3536
3776
|
var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
|
|
3537
3777
|
var LEVEL_COLORS = {
|
|
3538
3778
|
error: import_chalk.default.red.bold,
|
|
@@ -3563,8 +3803,7 @@ var Logger = class _Logger {
|
|
|
3563
3803
|
this.updateTransport();
|
|
3564
3804
|
}
|
|
3565
3805
|
/**
|
|
3566
|
-
* Configure logger options
|
|
3567
|
-
* Only parameters present in options are updated
|
|
3806
|
+
* Configure logger options. Accepts both LoggerOptions shape and flat param names (levels string, progressWithTimes, progressThrottleMs).
|
|
3568
3807
|
*/
|
|
3569
3808
|
configure(options) {
|
|
3570
3809
|
if (options.mode !== void 0) {
|
|
@@ -3574,32 +3813,24 @@ var Logger = class _Logger {
|
|
|
3574
3813
|
this.options.route = options.route;
|
|
3575
3814
|
this.updateTransport();
|
|
3576
3815
|
}
|
|
3577
|
-
if (options.prefix !== void 0)
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
if (options.
|
|
3581
|
-
this.options.silent = options.silent;
|
|
3582
|
-
}
|
|
3583
|
-
if (options.showLevel !== void 0) {
|
|
3584
|
-
this.options.showLevel = options.showLevel;
|
|
3585
|
-
}
|
|
3586
|
-
if (options.timestamp !== void 0) {
|
|
3587
|
-
this.options.timestamp = options.timestamp;
|
|
3588
|
-
}
|
|
3816
|
+
if (options.prefix !== void 0) this.options.prefix = options.prefix;
|
|
3817
|
+
if (options.silent !== void 0) this.options.silent = options.silent;
|
|
3818
|
+
if (options.showLevel !== void 0) this.options.showLevel = options.showLevel;
|
|
3819
|
+
if (options.timestamp !== void 0) this.options.timestamp = options.timestamp;
|
|
3589
3820
|
if (options.levels !== void 0) {
|
|
3590
|
-
|
|
3821
|
+
const levels = typeof options.levels === "string" ? options.levels.split(",") : options.levels;
|
|
3822
|
+
this.options.levels = this.normalizeLevels(levels);
|
|
3591
3823
|
}
|
|
3592
3824
|
if (options.progress !== void 0) {
|
|
3593
|
-
if (options.progress.withTimes !== void 0)
|
|
3594
|
-
|
|
3595
|
-
}
|
|
3596
|
-
if (options.progress.throttleMs !== void 0) {
|
|
3597
|
-
this.options.progressThrottle = options.progress.throttleMs;
|
|
3598
|
-
}
|
|
3825
|
+
if (options.progress.withTimes !== void 0) this.options.progressTimes = options.progress.withTimes;
|
|
3826
|
+
if (options.progress.throttleMs !== void 0) this.options.progressThrottle = options.progress.throttleMs;
|
|
3599
3827
|
}
|
|
3828
|
+
const flat = options;
|
|
3829
|
+
if (flat.progressWithTimes !== void 0) this.options.progressTimes = flat.progressWithTimes;
|
|
3830
|
+
if (flat.progressThrottleMs !== void 0) this.options.progressThrottle = flat.progressThrottleMs;
|
|
3600
3831
|
}
|
|
3601
3832
|
/**
|
|
3602
|
-
* Initialize logger from context and CLI parameters
|
|
3833
|
+
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
3603
3834
|
*/
|
|
3604
3835
|
static init(context, options) {
|
|
3605
3836
|
const paramDefs = {
|
|
@@ -3613,20 +3844,8 @@ var Logger = class _Logger {
|
|
|
3613
3844
|
progressWithTimes: "boolean default false",
|
|
3614
3845
|
progressThrottleMs: "number"
|
|
3615
3846
|
};
|
|
3616
|
-
const
|
|
3617
|
-
const config2 = {
|
|
3618
|
-
mode: options?.mode ?? cliParams.mode,
|
|
3619
|
-
route: options?.route ?? cliParams.route,
|
|
3620
|
-
prefix: options?.prefix ?? cliParams.prefix,
|
|
3621
|
-
silent: options?.silent ?? cliParams.silent,
|
|
3622
|
-
showLevel: options?.showLevel ?? cliParams.showLevel,
|
|
3623
|
-
timestamp: options?.timestamp ?? cliParams.timestamp,
|
|
3624
|
-
levels: options?.levels ?? (cliParams.levels ? cliParams.levels.split(",") : void 0),
|
|
3625
|
-
progress: options?.progress ?? {
|
|
3626
|
-
withTimes: cliParams.progressWithTimes,
|
|
3627
|
-
throttleMs: cliParams.progressThrottleMs
|
|
3628
|
-
}
|
|
3629
|
-
};
|
|
3847
|
+
const discovered = context.params.getAllForModule(paramDefs);
|
|
3848
|
+
const config2 = { ...discovered, ...options };
|
|
3630
3849
|
const logger = new _Logger(context, config2);
|
|
3631
3850
|
context.logger = logger;
|
|
3632
3851
|
return logger;
|
|
@@ -3639,7 +3858,7 @@ var Logger = class _Logger {
|
|
|
3639
3858
|
silent: false,
|
|
3640
3859
|
showLevel: false,
|
|
3641
3860
|
timestamp: false,
|
|
3642
|
-
levels:
|
|
3861
|
+
levels: DEFAULT_LEVELS,
|
|
3643
3862
|
progressTimes: false,
|
|
3644
3863
|
progressThrottle: void 0
|
|
3645
3864
|
};
|
|
@@ -3653,6 +3872,10 @@ var Logger = class _Logger {
|
|
|
3653
3872
|
}
|
|
3654
3873
|
this.options.mode = mode;
|
|
3655
3874
|
}
|
|
3875
|
+
/** Returns a styled string (bright white) for highlighting; keeps chalk inside logger. */
|
|
3876
|
+
highlight(text) {
|
|
3877
|
+
return import_chalk.default.whiteBright(text);
|
|
3878
|
+
}
|
|
3656
3879
|
debug(message, ...chunks) {
|
|
3657
3880
|
this.out({ level: "debug", message, chunks });
|
|
3658
3881
|
}
|
|
@@ -3795,15 +4018,17 @@ var Logger = class _Logger {
|
|
|
3795
4018
|
}
|
|
3796
4019
|
normalizeLevels(levels) {
|
|
3797
4020
|
if (!levels || !levels.length) {
|
|
3798
|
-
return
|
|
4021
|
+
return DEFAULT_LEVELS;
|
|
3799
4022
|
}
|
|
3800
|
-
const
|
|
3801
|
-
const
|
|
3802
|
-
const
|
|
4023
|
+
const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
|
|
4024
|
+
const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
|
|
4025
|
+
const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
|
|
4026
|
+
const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
|
|
4027
|
+
const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
|
|
3803
4028
|
if (unknown.length) {
|
|
3804
4029
|
console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
|
|
3805
4030
|
}
|
|
3806
|
-
const base =
|
|
4031
|
+
const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
|
|
3807
4032
|
return base.filter((level) => !excludes.includes(level));
|
|
3808
4033
|
}
|
|
3809
4034
|
isValidMode(mode) {
|
|
@@ -3828,12 +4053,7 @@ function extractComponentOptions(opts, componentName) {
|
|
|
3828
4053
|
return componentOptions;
|
|
3829
4054
|
}
|
|
3830
4055
|
function setup(opts = {}) {
|
|
3831
|
-
const args = Args.init({
|
|
3832
|
-
overrides: opts.overrides || {},
|
|
3833
|
-
defaults: opts.defaults || {}
|
|
3834
|
-
});
|
|
3835
4056
|
const partialContext = {
|
|
3836
|
-
args,
|
|
3837
4057
|
emitter: new import_events.EventEmitter(),
|
|
3838
4058
|
isStop: () => false,
|
|
3839
4059
|
cleanupFunctions: [],
|
|
@@ -3841,6 +4061,11 @@ function setup(opts = {}) {
|
|
|
3841
4061
|
partialContext.cleanupFunctions.push(fn);
|
|
3842
4062
|
}
|
|
3843
4063
|
};
|
|
4064
|
+
const args = Args.init(partialContext, {
|
|
4065
|
+
overrides: opts.overrides || {},
|
|
4066
|
+
defaults: opts.defaults || {}
|
|
4067
|
+
});
|
|
4068
|
+
partialContext.args = args;
|
|
3844
4069
|
const params = Params.init(partialContext, opts.overrides || {});
|
|
3845
4070
|
partialContext.params = params;
|
|
3846
4071
|
const loggerOptions = extractComponentOptions(opts, "logger");
|
|
@@ -3861,6 +4086,1107 @@ function setup(opts = {}) {
|
|
|
3861
4086
|
function setupContext(opts = {}) {
|
|
3862
4087
|
return setup(opts);
|
|
3863
4088
|
}
|
|
4089
|
+
|
|
4090
|
+
// src/utils/core-utils.ts
|
|
4091
|
+
function sleepMs(ms) {
|
|
4092
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4093
|
+
}
|
|
4094
|
+
function toJsonColumn(value) {
|
|
4095
|
+
if (value === void 0 || value === null) return null;
|
|
4096
|
+
return JSON.stringify(value);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// src/tasks/taskUtils.ts
|
|
4100
|
+
var import_node_crypto = require("crypto");
|
|
4101
|
+
function getDb(context) {
|
|
4102
|
+
const db = context.db;
|
|
4103
|
+
if (!db) {
|
|
4104
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4105
|
+
}
|
|
4106
|
+
return db;
|
|
4107
|
+
}
|
|
4108
|
+
function queueToTableNames(queue) {
|
|
4109
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4110
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4111
|
+
}
|
|
4112
|
+
return {
|
|
4113
|
+
tasksTable: queue,
|
|
4114
|
+
historyTable: `${queue}_history`
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
async function ensureTaskTables(context, options = {}) {
|
|
4118
|
+
const queue = options.queue ?? "tasks";
|
|
4119
|
+
const recreate = options.recreate ?? false;
|
|
4120
|
+
const db = getDb(context);
|
|
4121
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4122
|
+
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4123
|
+
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4124
|
+
if (recreate) {
|
|
4125
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
4126
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
4127
|
+
}
|
|
4128
|
+
if (needsTasks) {
|
|
4129
|
+
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4130
|
+
await db.schema.createTable(tasksTable, (t) => {
|
|
4131
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4132
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4133
|
+
t.timestamp("started_at");
|
|
4134
|
+
t.timestamp("completed_at");
|
|
4135
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4136
|
+
t.text("schedule");
|
|
4137
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4138
|
+
t.text("target").notNullable();
|
|
4139
|
+
t.text("task").notNullable();
|
|
4140
|
+
t.json("params");
|
|
4141
|
+
t.text("opid");
|
|
4142
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4143
|
+
t.text("progress");
|
|
4144
|
+
t.boolean("success");
|
|
4145
|
+
t.json("results");
|
|
4146
|
+
});
|
|
4147
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4148
|
+
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4149
|
+
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4150
|
+
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
4154
|
+
if (!tasksHasOpid) {
|
|
4155
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4156
|
+
t.text("opid");
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
4160
|
+
if (!tasksHasPausedAt) {
|
|
4161
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4162
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
if (needsHistory) {
|
|
4166
|
+
await db.schema.createTable(historyTable, (t) => {
|
|
4167
|
+
t.uuid("id").notNullable();
|
|
4168
|
+
t.timestamp("created_at").notNullable();
|
|
4169
|
+
t.timestamp("started_at");
|
|
4170
|
+
t.timestamp("completed_at");
|
|
4171
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4172
|
+
t.text("schedule");
|
|
4173
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4174
|
+
t.text("target").notNullable();
|
|
4175
|
+
t.text("task").notNullable();
|
|
4176
|
+
t.json("params");
|
|
4177
|
+
t.text("opid");
|
|
4178
|
+
t.text("progress");
|
|
4179
|
+
t.boolean("success");
|
|
4180
|
+
t.json("results");
|
|
4181
|
+
});
|
|
4182
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4183
|
+
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4184
|
+
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4185
|
+
});
|
|
4186
|
+
}
|
|
4187
|
+
const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
|
|
4188
|
+
if (!historyHasOpid) {
|
|
4189
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4190
|
+
t.text("opid");
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
async function enqueueTask(context, options) {
|
|
4195
|
+
const db = getDb(context);
|
|
4196
|
+
const queue = options.queue ?? "tasks";
|
|
4197
|
+
const { tasksTable } = queueToTableNames(queue);
|
|
4198
|
+
const id = (0, import_node_crypto.randomUUID)();
|
|
4199
|
+
await db(tasksTable).insert({
|
|
4200
|
+
id,
|
|
4201
|
+
target: options.target,
|
|
4202
|
+
task: options.task,
|
|
4203
|
+
params: toJsonColumn(options.params ?? null),
|
|
4204
|
+
opid: options.opid ?? null,
|
|
4205
|
+
priority: options.priority ?? 0,
|
|
4206
|
+
schedule: options.schedule ?? null
|
|
4207
|
+
});
|
|
4208
|
+
return id;
|
|
4209
|
+
}
|
|
4210
|
+
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
4211
|
+
const db = getDb(context);
|
|
4212
|
+
await db(tasksTable).where({ id: taskId }).update({
|
|
4213
|
+
progress: typeof progress === "string" ? progress : JSON.stringify(progress)
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// src/tasks/taskLogs.ts
|
|
4218
|
+
function getLogsState(context) {
|
|
4219
|
+
const holder = context;
|
|
4220
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4221
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4222
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4223
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4224
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4225
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4226
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4227
|
+
const errorDb = new FileDatabase({
|
|
4228
|
+
basePath,
|
|
4229
|
+
namespace,
|
|
4230
|
+
tableName: errorTableName,
|
|
4231
|
+
versioned: true,
|
|
4232
|
+
useMetadata: true,
|
|
4233
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4234
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4235
|
+
logger: holder.logger
|
|
4236
|
+
});
|
|
4237
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4238
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4239
|
+
if (!enabled) {
|
|
4240
|
+
const disabledState = {
|
|
4241
|
+
db: null,
|
|
4242
|
+
errorDb,
|
|
4243
|
+
queue: Promise.resolve(),
|
|
4244
|
+
initialized: true,
|
|
4245
|
+
errorInitialized: false
|
|
4246
|
+
};
|
|
4247
|
+
holder.__tasksLogsState = disabledState;
|
|
4248
|
+
return disabledState;
|
|
4249
|
+
}
|
|
4250
|
+
const db = new FileDatabase({
|
|
4251
|
+
basePath,
|
|
4252
|
+
namespace,
|
|
4253
|
+
tableName,
|
|
4254
|
+
versioned: true,
|
|
4255
|
+
useMetadata: true,
|
|
4256
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4257
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4258
|
+
logger: holder.logger
|
|
4259
|
+
});
|
|
4260
|
+
const state = {
|
|
4261
|
+
db,
|
|
4262
|
+
errorDb,
|
|
4263
|
+
queue: Promise.resolve(),
|
|
4264
|
+
initialized: false,
|
|
4265
|
+
errorInitialized: false
|
|
4266
|
+
};
|
|
4267
|
+
holder.__tasksLogsState = state;
|
|
4268
|
+
return state;
|
|
4269
|
+
}
|
|
4270
|
+
function isErrorPayload(payload) {
|
|
4271
|
+
if (!payload) return false;
|
|
4272
|
+
if (typeof payload === "object") {
|
|
4273
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4274
|
+
if (level === "error" || level === "fatal") return true;
|
|
4275
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4276
|
+
return false;
|
|
4277
|
+
}
|
|
4278
|
+
if (typeof payload === "string") {
|
|
4279
|
+
return /\berror\b/i.test(payload);
|
|
4280
|
+
}
|
|
4281
|
+
return false;
|
|
4282
|
+
}
|
|
4283
|
+
function buildLogRecord(task, payload) {
|
|
4284
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4285
|
+
return {
|
|
4286
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4287
|
+
opid: task.opid ?? null,
|
|
4288
|
+
taskId: task.id,
|
|
4289
|
+
taskName: task.task,
|
|
4290
|
+
target: task.target,
|
|
4291
|
+
source: typeof params.source === "string" ? params.source : null,
|
|
4292
|
+
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4293
|
+
payload
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
function appendTaskIpcLog(context, task, payload) {
|
|
4297
|
+
const state = getLogsState(context);
|
|
4298
|
+
if (!state.db && !state.errorDb) return;
|
|
4299
|
+
const record = buildLogRecord(task, payload);
|
|
4300
|
+
state.queue = state.queue.then(async () => {
|
|
4301
|
+
if (state.db) {
|
|
4302
|
+
await state.db.write([record], { forceNewVersion: !state.initialized });
|
|
4303
|
+
state.initialized = true;
|
|
4304
|
+
}
|
|
4305
|
+
if (state.errorDb && isErrorPayload(payload)) {
|
|
4306
|
+
await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
|
|
4307
|
+
state.errorInitialized = true;
|
|
4308
|
+
}
|
|
4309
|
+
}).catch((error) => {
|
|
4310
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// src/tasks/time-matcher.ts
|
|
4315
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4316
|
+
function resolveAsterisks(field, range) {
|
|
4317
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4318
|
+
}
|
|
4319
|
+
function resolveRanges(field) {
|
|
4320
|
+
const regex = /(\d+)-(\d+)/;
|
|
4321
|
+
let current = field;
|
|
4322
|
+
while (true) {
|
|
4323
|
+
const match = regex.exec(current);
|
|
4324
|
+
if (!match) break;
|
|
4325
|
+
const raw = match[0];
|
|
4326
|
+
let first = Number(match[1]);
|
|
4327
|
+
let last = Number(match[2]);
|
|
4328
|
+
if (last < first) {
|
|
4329
|
+
[first, last] = [last, first];
|
|
4330
|
+
}
|
|
4331
|
+
const values = [];
|
|
4332
|
+
for (let i = first; i <= last; i += 1) {
|
|
4333
|
+
values.push(i);
|
|
4334
|
+
}
|
|
4335
|
+
current = current.replace(raw, values.join(","));
|
|
4336
|
+
}
|
|
4337
|
+
return current;
|
|
4338
|
+
}
|
|
4339
|
+
function resolveSteps(field) {
|
|
4340
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4341
|
+
if (!match) return field;
|
|
4342
|
+
const base = match[1];
|
|
4343
|
+
const step = Number(match[2]);
|
|
4344
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4345
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4346
|
+
}
|
|
4347
|
+
function convertPattern(pattern) {
|
|
4348
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4349
|
+
if (parts.length !== 6) {
|
|
4350
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4351
|
+
}
|
|
4352
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4353
|
+
}
|
|
4354
|
+
function fieldMatches(field, value) {
|
|
4355
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4356
|
+
return allowed.includes(value);
|
|
4357
|
+
}
|
|
4358
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4359
|
+
const parsed = convertPattern(pattern);
|
|
4360
|
+
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
// src/tasks/TaskMaster.ts
|
|
4364
|
+
var TaskMaster = class {
|
|
4365
|
+
context;
|
|
4366
|
+
task;
|
|
4367
|
+
constructor(context, task) {
|
|
4368
|
+
this.context = context;
|
|
4369
|
+
this.task = task;
|
|
4370
|
+
}
|
|
4371
|
+
cantRunReason() {
|
|
4372
|
+
return false;
|
|
4373
|
+
}
|
|
4374
|
+
requestStop(_allowanceMs) {
|
|
4375
|
+
}
|
|
4376
|
+
};
|
|
4377
|
+
|
|
4378
|
+
// src/tasks/coreTasks/TaskPing.ts
|
|
4379
|
+
var TaskPing = class extends TaskMaster {
|
|
4380
|
+
async run() {
|
|
4381
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4382
|
+
return { success: true, results: "pong" };
|
|
4383
|
+
}
|
|
4384
|
+
};
|
|
4385
|
+
|
|
4386
|
+
// src/tasks/coreTasks/TaskSampleProcess.ts
|
|
4387
|
+
var TaskSampleProcess = class extends TaskMaster {
|
|
4388
|
+
stopRequested = false;
|
|
4389
|
+
stopAllowanceMs = 0;
|
|
4390
|
+
stopDecisionLogged = false;
|
|
4391
|
+
requestStop(allowanceMs) {
|
|
4392
|
+
this.stopRequested = true;
|
|
4393
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
4394
|
+
this.context.logger.warn?.(
|
|
4395
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4396
|
+
);
|
|
4397
|
+
}
|
|
4398
|
+
async run(reportProgress) {
|
|
4399
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
4400
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
4401
|
+
const nameRaw = this.task?.params?.name;
|
|
4402
|
+
const total = Number(totalRaw);
|
|
4403
|
+
const delay = Number(delayRaw);
|
|
4404
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
4405
|
+
const errors = [];
|
|
4406
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4407
|
+
errors.push('param "total" must be a positive integer');
|
|
4408
|
+
}
|
|
4409
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4410
|
+
errors.push('param "delay" must be an integer >= 0');
|
|
4411
|
+
}
|
|
4412
|
+
if (errors.length > 0) {
|
|
4413
|
+
return {
|
|
4414
|
+
success: false,
|
|
4415
|
+
results: {
|
|
4416
|
+
error: `Validation failed: ${errors.join(", ")}`,
|
|
4417
|
+
received: { total: totalRaw, delay: delayRaw, name: nameRaw }
|
|
4418
|
+
}
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
const startedAt = Date.now();
|
|
4422
|
+
for (let i = 1; i <= total; i += 1) {
|
|
4423
|
+
if (this.stopRequested) {
|
|
4424
|
+
const remainingMs = Math.max(0, (total - i + 1) * delay);
|
|
4425
|
+
if (remainingMs <= this.stopAllowanceMs) {
|
|
4426
|
+
if (!this.stopDecisionLogged) {
|
|
4427
|
+
this.stopDecisionLogged = true;
|
|
4428
|
+
this.context.logger.warn?.(
|
|
4429
|
+
`[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
|
|
4430
|
+
);
|
|
4431
|
+
}
|
|
4432
|
+
} else {
|
|
4433
|
+
this.context.logger.warn?.(
|
|
4434
|
+
`[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
|
|
4435
|
+
);
|
|
4436
|
+
return {
|
|
4437
|
+
success: false,
|
|
4438
|
+
results: {
|
|
4439
|
+
message: `Stopped before completion at iteration ${i}/${total}`,
|
|
4440
|
+
completed: i - 1,
|
|
4441
|
+
total,
|
|
4442
|
+
name,
|
|
4443
|
+
remainingMs,
|
|
4444
|
+
allowanceMs: this.stopAllowanceMs
|
|
4445
|
+
}
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
const elapsed = Date.now() - startedAt;
|
|
4450
|
+
const remaining = Math.max(0, (total - i) * delay);
|
|
4451
|
+
const progress = {
|
|
4452
|
+
name,
|
|
4453
|
+
count: i,
|
|
4454
|
+
total,
|
|
4455
|
+
elapsedMs: elapsed,
|
|
4456
|
+
remainingMs: remaining,
|
|
4457
|
+
status: `running ${name}: ${i}/${total}`
|
|
4458
|
+
};
|
|
4459
|
+
this.context.logger.progress("running", {
|
|
4460
|
+
prefix: name,
|
|
4461
|
+
count: i,
|
|
4462
|
+
total
|
|
4463
|
+
});
|
|
4464
|
+
await reportProgress(progress);
|
|
4465
|
+
await sleepMs(delay);
|
|
4466
|
+
}
|
|
4467
|
+
return {
|
|
4468
|
+
success: true,
|
|
4469
|
+
results: {
|
|
4470
|
+
message: `Completed ${total} iterations`,
|
|
4471
|
+
total,
|
|
4472
|
+
delay,
|
|
4473
|
+
name
|
|
4474
|
+
}
|
|
4475
|
+
};
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
|
|
4479
|
+
// src/tasks/coreTasks/TaskShellCommand.ts
|
|
4480
|
+
var import_node_child_process = require("child_process");
|
|
4481
|
+
function runShellCommand(command, cwd) {
|
|
4482
|
+
return new Promise((resolve2, reject) => {
|
|
4483
|
+
const child = (0, import_node_child_process.spawn)(command, {
|
|
4484
|
+
shell: true,
|
|
4485
|
+
cwd: cwd || process.cwd(),
|
|
4486
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4487
|
+
});
|
|
4488
|
+
let output = "";
|
|
4489
|
+
let stderr = "";
|
|
4490
|
+
child.stdout.on("data", (chunk) => {
|
|
4491
|
+
output += String(chunk);
|
|
4492
|
+
});
|
|
4493
|
+
child.stderr.on("data", (chunk) => {
|
|
4494
|
+
stderr += String(chunk);
|
|
4495
|
+
});
|
|
4496
|
+
child.on("error", (error) => {
|
|
4497
|
+
reject(error);
|
|
4498
|
+
});
|
|
4499
|
+
child.on("close", (exitCode, signal) => {
|
|
4500
|
+
resolve2({
|
|
4501
|
+
exitCode,
|
|
4502
|
+
output: output.trim(),
|
|
4503
|
+
stderr: stderr.trim(),
|
|
4504
|
+
signal
|
|
4505
|
+
});
|
|
4506
|
+
});
|
|
4507
|
+
});
|
|
4508
|
+
}
|
|
4509
|
+
var TaskShellCommand = class extends TaskMaster {
|
|
4510
|
+
async run() {
|
|
4511
|
+
const params = this.task?.params;
|
|
4512
|
+
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
4513
|
+
const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
|
|
4514
|
+
const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
|
|
4515
|
+
const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
|
|
4516
|
+
if (!command) {
|
|
4517
|
+
return {
|
|
4518
|
+
success: false,
|
|
4519
|
+
results: {
|
|
4520
|
+
error: 'Validation failed: param "command" must be a non-empty string',
|
|
4521
|
+
received: this.task?.params ?? null
|
|
4522
|
+
}
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
try {
|
|
4526
|
+
const result = await runShellCommand(command, cwd);
|
|
4527
|
+
const success = result.exitCode === 0;
|
|
4528
|
+
this.context.logger.info?.(
|
|
4529
|
+
`[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
|
|
4530
|
+
);
|
|
4531
|
+
return {
|
|
4532
|
+
success,
|
|
4533
|
+
results: {
|
|
4534
|
+
command,
|
|
4535
|
+
cwd: cwd ?? process.cwd(),
|
|
4536
|
+
output: result.output,
|
|
4537
|
+
stderr: result.stderr,
|
|
4538
|
+
exitCode: result.exitCode,
|
|
4539
|
+
signal: result.signal
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
return {
|
|
4544
|
+
success: false,
|
|
4545
|
+
results: {
|
|
4546
|
+
command,
|
|
4547
|
+
cwd: cwd ?? process.cwd(),
|
|
4548
|
+
output: "",
|
|
4549
|
+
stderr: "",
|
|
4550
|
+
exitCode: null,
|
|
4551
|
+
error: error?.message ?? String(error)
|
|
4552
|
+
}
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
};
|
|
4557
|
+
|
|
4558
|
+
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
4559
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
4560
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
4561
|
+
function toGb(valueBytes) {
|
|
4562
|
+
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4563
|
+
}
|
|
4564
|
+
function toMb(valueBytes) {
|
|
4565
|
+
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4566
|
+
}
|
|
4567
|
+
async function getDiskStats() {
|
|
4568
|
+
const stats = await import_promises.default.statfs("/");
|
|
4569
|
+
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4570
|
+
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4571
|
+
const used = total - free;
|
|
4572
|
+
return {
|
|
4573
|
+
total: toGb(total),
|
|
4574
|
+
used: toGb(used),
|
|
4575
|
+
free: toGb(free)
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
var TaskSystemInfo = class extends TaskMaster {
|
|
4579
|
+
async run() {
|
|
4580
|
+
try {
|
|
4581
|
+
const totalMemory = import_node_os.default.totalmem();
|
|
4582
|
+
const freeMemory = import_node_os.default.freemem();
|
|
4583
|
+
const usedMemory = totalMemory - freeMemory;
|
|
4584
|
+
const cpus = import_node_os.default.cpus();
|
|
4585
|
+
const cpuUtilization = cpus.map((cpu) => {
|
|
4586
|
+
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4587
|
+
const usage = (total - cpu.times.idle) / total * 100;
|
|
4588
|
+
return Number(usage.toFixed(2));
|
|
4589
|
+
});
|
|
4590
|
+
const processMemory = process.memoryUsage();
|
|
4591
|
+
const disk = await getDiskStats();
|
|
4592
|
+
const results = {
|
|
4593
|
+
memory: {
|
|
4594
|
+
total: toGb(totalMemory),
|
|
4595
|
+
used: toGb(usedMemory),
|
|
4596
|
+
free: toGb(freeMemory)
|
|
4597
|
+
},
|
|
4598
|
+
processMemory: {
|
|
4599
|
+
rss: toMb(processMemory.rss),
|
|
4600
|
+
heapTotal: toMb(processMemory.heapTotal),
|
|
4601
|
+
heapUsed: toMb(processMemory.heapUsed),
|
|
4602
|
+
external: toMb(processMemory.external)
|
|
4603
|
+
},
|
|
4604
|
+
disk,
|
|
4605
|
+
cpu: {
|
|
4606
|
+
cores: cpuUtilization.length,
|
|
4607
|
+
utilization: cpuUtilization
|
|
4608
|
+
},
|
|
4609
|
+
runtime: {
|
|
4610
|
+
platform: import_node_os.default.platform(),
|
|
4611
|
+
arch: import_node_os.default.arch(),
|
|
4612
|
+
uptimeSec: import_node_os.default.uptime(),
|
|
4613
|
+
hostname: import_node_os.default.hostname()
|
|
4614
|
+
}
|
|
4615
|
+
};
|
|
4616
|
+
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
4617
|
+
return { success: true, results };
|
|
4618
|
+
} catch (error) {
|
|
4619
|
+
return {
|
|
4620
|
+
success: false,
|
|
4621
|
+
results: {
|
|
4622
|
+
error: "Can't collect system stats",
|
|
4623
|
+
message: error?.message ?? String(error)
|
|
4624
|
+
}
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
};
|
|
4629
|
+
|
|
4630
|
+
// src/tasks/coreTasks/TaskSumAB.ts
|
|
4631
|
+
var TaskSumAB = class extends TaskMaster {
|
|
4632
|
+
async run() {
|
|
4633
|
+
const a = this.task?.params?.a;
|
|
4634
|
+
const b = this.task?.params?.b;
|
|
4635
|
+
if (typeof a !== "number" || Number.isNaN(a)) {
|
|
4636
|
+
return {
|
|
4637
|
+
success: false,
|
|
4638
|
+
results: {
|
|
4639
|
+
error: 'Validation failed: param "a" must be a valid number',
|
|
4640
|
+
received: { a, b }
|
|
4641
|
+
}
|
|
4642
|
+
};
|
|
4643
|
+
}
|
|
4644
|
+
if (typeof b !== "number" || Number.isNaN(b)) {
|
|
4645
|
+
return {
|
|
4646
|
+
success: false,
|
|
4647
|
+
results: {
|
|
4648
|
+
error: 'Validation failed: param "b" must be a valid number',
|
|
4649
|
+
received: { a, b }
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
const sum = a + b;
|
|
4654
|
+
this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
|
|
4655
|
+
return {
|
|
4656
|
+
success: true,
|
|
4657
|
+
results: { a, b, sum }
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
};
|
|
4661
|
+
|
|
4662
|
+
// src/tasks/coreTasks/TaskStopRunner.ts
|
|
4663
|
+
var TaskStopRunner = class extends TaskMaster {
|
|
4664
|
+
async run() {
|
|
4665
|
+
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4666
|
+
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
4667
|
+
return {
|
|
4668
|
+
success: true,
|
|
4669
|
+
results: {
|
|
4670
|
+
stopRunner: true,
|
|
4671
|
+
allowanceMs,
|
|
4672
|
+
message: "Runner stop requested"
|
|
4673
|
+
}
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
};
|
|
4677
|
+
|
|
4678
|
+
// src/tasks/TasksRegistry.ts
|
|
4679
|
+
var TasksRegistry = class _TasksRegistry {
|
|
4680
|
+
map = {};
|
|
4681
|
+
constructor(initial) {
|
|
4682
|
+
if (initial) {
|
|
4683
|
+
this.addMany(initial);
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
static withCoreTasks() {
|
|
4687
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
4688
|
+
}
|
|
4689
|
+
add(taskName, taskClass) {
|
|
4690
|
+
this.map[taskName] = taskClass;
|
|
4691
|
+
return this;
|
|
4692
|
+
}
|
|
4693
|
+
addMany(entries) {
|
|
4694
|
+
for (const [name, klass] of Object.entries(entries)) {
|
|
4695
|
+
this.add(name, klass);
|
|
4696
|
+
}
|
|
4697
|
+
return this;
|
|
4698
|
+
}
|
|
4699
|
+
get(taskName) {
|
|
4700
|
+
return this.map[taskName];
|
|
4701
|
+
}
|
|
4702
|
+
listSupportedTasks() {
|
|
4703
|
+
return Object.keys(this.map).sort();
|
|
4704
|
+
}
|
|
4705
|
+
toObject() {
|
|
4706
|
+
return { ...this.map };
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
|
|
4710
|
+
// src/tasks/taskScriptRunner.ts
|
|
4711
|
+
var import_node_child_process2 = require("child_process");
|
|
4712
|
+
function toCliArgs(args = []) {
|
|
4713
|
+
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4714
|
+
}
|
|
4715
|
+
function formatChildLogPrefix(task) {
|
|
4716
|
+
return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4717
|
+
}
|
|
4718
|
+
async function runNodeTaskScript(context, options) {
|
|
4719
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
4720
|
+
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4721
|
+
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
4722
|
+
const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
|
|
4723
|
+
const child = (0, import_node_child_process2.spawn)(
|
|
4724
|
+
process.execPath,
|
|
4725
|
+
nodeArgs,
|
|
4726
|
+
{
|
|
4727
|
+
cwd: options.cwd || process.cwd(),
|
|
4728
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
4729
|
+
env: {
|
|
4730
|
+
...process.env,
|
|
4731
|
+
TASK_ID: options.task.id,
|
|
4732
|
+
TASK_NAME: options.task.task,
|
|
4733
|
+
TASK_OPID: options.task.opid || ""
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
);
|
|
4737
|
+
let stdout = "";
|
|
4738
|
+
let stderr = "";
|
|
4739
|
+
let workerResult = null;
|
|
4740
|
+
let hadErrorMessage = false;
|
|
4741
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
4742
|
+
const db = context.db;
|
|
4743
|
+
const tasksTable = context.params?.get?.("table") || "tasks";
|
|
4744
|
+
let progressWriteChain = Promise.resolve();
|
|
4745
|
+
let progressCallbackChain = Promise.resolve();
|
|
4746
|
+
const updateProgress = (text) => {
|
|
4747
|
+
if (!db || !text || !text.trim()) return;
|
|
4748
|
+
progressWriteChain = progressWriteChain.then(async () => {
|
|
4749
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
4750
|
+
}).catch((error) => {
|
|
4751
|
+
context.logger.warn?.(
|
|
4752
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4753
|
+
);
|
|
4754
|
+
});
|
|
4755
|
+
if (options.onProgress) {
|
|
4756
|
+
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
4757
|
+
await options.onProgress?.(text.slice(0, 4e3));
|
|
4758
|
+
}).catch((error) => {
|
|
4759
|
+
context.logger.warn?.(
|
|
4760
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4761
|
+
);
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
};
|
|
4765
|
+
const payloadToProgressText = (payload) => {
|
|
4766
|
+
if (!payload) return "";
|
|
4767
|
+
if (typeof payload === "string") return payload;
|
|
4768
|
+
if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4769
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4770
|
+
return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
|
|
4771
|
+
}
|
|
4772
|
+
if (typeof payload.message === "string") return payload.message;
|
|
4773
|
+
if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4774
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4775
|
+
return `${pfx}${payload.count}/${payload.total}`;
|
|
4776
|
+
}
|
|
4777
|
+
return "";
|
|
4778
|
+
};
|
|
4779
|
+
child.stdout.on("data", (chunk) => {
|
|
4780
|
+
const text = String(chunk);
|
|
4781
|
+
stdout += text;
|
|
4782
|
+
if (text.trim()) {
|
|
4783
|
+
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4784
|
+
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4785
|
+
}
|
|
4786
|
+
});
|
|
4787
|
+
child.stderr.on("data", (chunk) => {
|
|
4788
|
+
const text = String(chunk);
|
|
4789
|
+
stderr += text;
|
|
4790
|
+
if (text.trim()) {
|
|
4791
|
+
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4792
|
+
}
|
|
4793
|
+
});
|
|
4794
|
+
child.on("message", (message) => {
|
|
4795
|
+
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4796
|
+
workerResult = message.__taskWorkerResult;
|
|
4797
|
+
return;
|
|
4798
|
+
}
|
|
4799
|
+
if (message && typeof message === "object") {
|
|
4800
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4801
|
+
if (level === "error" || level === "fatal") {
|
|
4802
|
+
hadErrorMessage = true;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
appendTaskIpcLog(context, options.task, message);
|
|
4806
|
+
const progressText = payloadToProgressText(message);
|
|
4807
|
+
if (progressText) {
|
|
4808
|
+
updateProgress(progressText);
|
|
4809
|
+
if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
|
|
4810
|
+
const countNum = Number(String(message.count).trim());
|
|
4811
|
+
const totalNum = Number(message.total);
|
|
4812
|
+
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
4813
|
+
context.logger.progress(message.message || "progress", {
|
|
4814
|
+
prefix: message.prefix || prefix,
|
|
4815
|
+
count: countNum,
|
|
4816
|
+
total: totalNum
|
|
4817
|
+
});
|
|
4818
|
+
} else {
|
|
4819
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4820
|
+
}
|
|
4821
|
+
} else {
|
|
4822
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
});
|
|
4826
|
+
return await new Promise((resolve2, reject) => {
|
|
4827
|
+
child.on("error", (error) => reject(error));
|
|
4828
|
+
child.on("close", (exitCode, signal) => {
|
|
4829
|
+
Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
|
|
4830
|
+
resolve2({
|
|
4831
|
+
exitCode,
|
|
4832
|
+
signal,
|
|
4833
|
+
stdout: stdout.trim(),
|
|
4834
|
+
stderr: stderr.trim(),
|
|
4835
|
+
workerResult,
|
|
4836
|
+
hadErrorMessage
|
|
4837
|
+
});
|
|
4838
|
+
});
|
|
4839
|
+
});
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
// src/tasks/index.ts
|
|
4844
|
+
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4845
|
+
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4846
|
+
function getDb2(context) {
|
|
4847
|
+
const db = context.db;
|
|
4848
|
+
if (!db) {
|
|
4849
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4850
|
+
}
|
|
4851
|
+
return db;
|
|
4852
|
+
}
|
|
4853
|
+
function normalizeRegistry(registry) {
|
|
4854
|
+
if (!registry) return defaultTasksRegistry;
|
|
4855
|
+
if (registry instanceof TasksRegistry) return registry;
|
|
4856
|
+
return new TasksRegistry().addMany(registry);
|
|
4857
|
+
}
|
|
4858
|
+
function normalizeAllowedTasks(value) {
|
|
4859
|
+
if (!value) return void 0;
|
|
4860
|
+
if (Array.isArray(value)) {
|
|
4861
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4862
|
+
return out2.length ? out2 : void 0;
|
|
4863
|
+
}
|
|
4864
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4865
|
+
return out.length ? out : void 0;
|
|
4866
|
+
}
|
|
4867
|
+
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
4868
|
+
return enqueueTask(context, {
|
|
4869
|
+
queue,
|
|
4870
|
+
target,
|
|
4871
|
+
task: "stopRunner",
|
|
4872
|
+
params: { allowanceMs },
|
|
4873
|
+
priority: 1e6
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4877
|
+
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4878
|
+
for (const [, taskInstance] of runningTaskInstances) {
|
|
4879
|
+
if (typeof taskInstance.requestStop === "function") {
|
|
4880
|
+
try {
|
|
4881
|
+
await taskInstance.requestStop(allowanceMs);
|
|
4882
|
+
} catch (error) {
|
|
4883
|
+
context.logger.warn?.("[tasks] task requestStop failed:", error);
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
context.emitter.emit("stop", allowanceMs);
|
|
4888
|
+
}
|
|
4889
|
+
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4890
|
+
const db = getDb2(context);
|
|
4891
|
+
const taskName = row.task;
|
|
4892
|
+
const TaskClass = registry.get(taskName);
|
|
4893
|
+
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4894
|
+
if (!TaskClass) {
|
|
4895
|
+
const err = { message: `Unknown task "${taskName}"` };
|
|
4896
|
+
await db(historyTable).insert({
|
|
4897
|
+
...rowForHistory,
|
|
4898
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4899
|
+
success: false,
|
|
4900
|
+
params: toJsonColumn(row.params),
|
|
4901
|
+
results: toJsonColumn(err)
|
|
4902
|
+
});
|
|
4903
|
+
if (row.schedule) {
|
|
4904
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4905
|
+
started_at: null,
|
|
4906
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4907
|
+
success: false,
|
|
4908
|
+
results: toJsonColumn(err),
|
|
4909
|
+
past_due: null,
|
|
4910
|
+
paused_at: db.fn.now(),
|
|
4911
|
+
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4912
|
+
});
|
|
4913
|
+
} else {
|
|
4914
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4915
|
+
}
|
|
4916
|
+
return { stopRunnerRequested: false, stopAllowanceMs: 0 };
|
|
4917
|
+
}
|
|
4918
|
+
let success = false;
|
|
4919
|
+
let results = null;
|
|
4920
|
+
let taskInstance = null;
|
|
4921
|
+
try {
|
|
4922
|
+
taskInstance = new TaskClass(context, row);
|
|
4923
|
+
runningTaskInstances.set(row.id, taskInstance);
|
|
4924
|
+
const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
|
|
4925
|
+
success = !!runResult?.success;
|
|
4926
|
+
results = runResult?.results ?? null;
|
|
4927
|
+
} catch (error) {
|
|
4928
|
+
success = false;
|
|
4929
|
+
results = {
|
|
4930
|
+
message: error?.message ?? String(error),
|
|
4931
|
+
name: error?.name ?? "Error",
|
|
4932
|
+
stack: error?.stack ?? null
|
|
4933
|
+
};
|
|
4934
|
+
} finally {
|
|
4935
|
+
runningTaskInstances.delete(row.id);
|
|
4936
|
+
}
|
|
4937
|
+
await db(historyTable).insert({
|
|
4938
|
+
...rowForHistory,
|
|
4939
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4940
|
+
success,
|
|
4941
|
+
params: toJsonColumn(row.params),
|
|
4942
|
+
results: toJsonColumn(results)
|
|
4943
|
+
});
|
|
4944
|
+
if (!success) {
|
|
4945
|
+
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4946
|
+
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
4947
|
+
const fallbackRecoverCommand = [
|
|
4948
|
+
"npx",
|
|
4949
|
+
"tsx",
|
|
4950
|
+
"examples/tasks/recover-task.ts",
|
|
4951
|
+
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
4952
|
+
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
4953
|
+
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
4954
|
+
].join(" ");
|
|
4955
|
+
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4956
|
+
appendTaskIpcLog(context, row, {
|
|
4957
|
+
level: "error",
|
|
4958
|
+
message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4959
|
+
details: results
|
|
4960
|
+
});
|
|
4961
|
+
}
|
|
4962
|
+
if (row.schedule) {
|
|
4963
|
+
if (success) {
|
|
4964
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4965
|
+
started_at: null,
|
|
4966
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4967
|
+
success,
|
|
4968
|
+
results: toJsonColumn(results),
|
|
4969
|
+
progress: null,
|
|
4970
|
+
past_due: null
|
|
4971
|
+
});
|
|
4972
|
+
} else {
|
|
4973
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4974
|
+
started_at: null,
|
|
4975
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4976
|
+
success,
|
|
4977
|
+
results: toJsonColumn(results),
|
|
4978
|
+
paused_at: db.fn.now(),
|
|
4979
|
+
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4980
|
+
past_due: null
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
} else {
|
|
4984
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4985
|
+
}
|
|
4986
|
+
const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
|
|
4987
|
+
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4988
|
+
return { stopRunnerRequested, stopAllowanceMs };
|
|
4989
|
+
}
|
|
4990
|
+
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
4991
|
+
const db = getDb2(context);
|
|
4992
|
+
let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
4993
|
+
if (taskNames && taskNames.length > 0) {
|
|
4994
|
+
query = query.whereIn("task", taskNames);
|
|
4995
|
+
}
|
|
4996
|
+
const candidates = await query;
|
|
4997
|
+
for (const row of candidates) {
|
|
4998
|
+
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4999
|
+
continue;
|
|
5000
|
+
}
|
|
5001
|
+
const TaskClass = registry.get(row.task);
|
|
5002
|
+
if (TaskClass) {
|
|
5003
|
+
const taskInstance = new TaskClass(context, row);
|
|
5004
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5005
|
+
if (reason) {
|
|
5006
|
+
if (!row.past_due) {
|
|
5007
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5008
|
+
past_due: db.fn.now(),
|
|
5009
|
+
progress: String(reason)
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
continue;
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
|
|
5016
|
+
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5017
|
+
if (claimed) return claimed;
|
|
5018
|
+
}
|
|
5019
|
+
return null;
|
|
5020
|
+
}
|
|
5021
|
+
async function runTasksLoop(context, options) {
|
|
5022
|
+
const queue = options.queue ?? "tasks";
|
|
5023
|
+
const target = options.target;
|
|
5024
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
5025
|
+
const maxParallel = options.maxParallel ?? 1;
|
|
5026
|
+
const scanLimit = options.scanLimit ?? 100;
|
|
5027
|
+
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5028
|
+
const registry = normalizeRegistry(options.registry);
|
|
5029
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5030
|
+
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5031
|
+
const runningPromises = /* @__PURE__ */ new Set();
|
|
5032
|
+
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5033
|
+
let runningStopControlPromise = null;
|
|
5034
|
+
let stopRequested = false;
|
|
5035
|
+
let stopAllowanceMs = 5e3;
|
|
5036
|
+
context.__tasksRunnerStop = false;
|
|
5037
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
5038
|
+
if (!runningStopControlPromise) {
|
|
5039
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
5040
|
+
context,
|
|
5041
|
+
tasksTable,
|
|
5042
|
+
target,
|
|
5043
|
+
registry,
|
|
5044
|
+
10,
|
|
5045
|
+
["stopRunner", "stop"]
|
|
5046
|
+
);
|
|
5047
|
+
if (claimedStopTask) {
|
|
5048
|
+
runningStopControlPromise = executeClaimedTask(
|
|
5049
|
+
context,
|
|
5050
|
+
tasksTable,
|
|
5051
|
+
historyTable,
|
|
5052
|
+
claimedStopTask,
|
|
5053
|
+
registry,
|
|
5054
|
+
runningTaskInstances
|
|
5055
|
+
).then(async (outcome) => {
|
|
5056
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5057
|
+
stopRequested = true;
|
|
5058
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5059
|
+
context.__tasksRunnerStop = true;
|
|
5060
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5061
|
+
}
|
|
5062
|
+
}).finally(() => {
|
|
5063
|
+
runningStopControlPromise = null;
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
while (runningPromises.size < maxParallel) {
|
|
5068
|
+
const claimed = await claimNextRunnableTask(
|
|
5069
|
+
context,
|
|
5070
|
+
tasksTable,
|
|
5071
|
+
target,
|
|
5072
|
+
registry,
|
|
5073
|
+
scanLimit,
|
|
5074
|
+
allowedTasks
|
|
5075
|
+
);
|
|
5076
|
+
if (!claimed) break;
|
|
5077
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5078
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5079
|
+
stopRequested = true;
|
|
5080
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5081
|
+
context.__tasksRunnerStop = true;
|
|
5082
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5083
|
+
}
|
|
5084
|
+
}).finally(() => {
|
|
5085
|
+
runningPromises.delete(p);
|
|
5086
|
+
});
|
|
5087
|
+
runningPromises.add(p);
|
|
5088
|
+
}
|
|
5089
|
+
await sleepMs(pollMs);
|
|
5090
|
+
}
|
|
5091
|
+
if (context.isStop() && !stopRequested) {
|
|
5092
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
5093
|
+
}
|
|
5094
|
+
if (runningPromises.size > 0) {
|
|
5095
|
+
if (stopRequested) {
|
|
5096
|
+
await Promise.race([
|
|
5097
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
5098
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
5099
|
+
context.logger.warn?.(
|
|
5100
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
5101
|
+
);
|
|
5102
|
+
})
|
|
5103
|
+
]);
|
|
5104
|
+
} else {
|
|
5105
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5110
|
+
const db = getDb2(context);
|
|
5111
|
+
const queue = options.queue ?? "tasks";
|
|
5112
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5113
|
+
const pollMs = options.pollMs ?? 500;
|
|
5114
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5115
|
+
const deadline = Date.now() + timeoutMs;
|
|
5116
|
+
while (Date.now() <= deadline) {
|
|
5117
|
+
const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5118
|
+
if (done) return done;
|
|
5119
|
+
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5120
|
+
if (!pending) {
|
|
5121
|
+
const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5122
|
+
return maybeDone ?? null;
|
|
5123
|
+
}
|
|
5124
|
+
await sleepMs(pollMs);
|
|
5125
|
+
}
|
|
5126
|
+
return null;
|
|
5127
|
+
}
|
|
5128
|
+
var TasksManager = class _TasksManager {
|
|
5129
|
+
context;
|
|
5130
|
+
queue;
|
|
5131
|
+
target;
|
|
5132
|
+
recreateTaskTables;
|
|
5133
|
+
pollMs;
|
|
5134
|
+
maxParallel;
|
|
5135
|
+
scanLimit;
|
|
5136
|
+
allowedTasks;
|
|
5137
|
+
registry;
|
|
5138
|
+
constructor(context, options = {}) {
|
|
5139
|
+
this.context = context;
|
|
5140
|
+
this.queue = options.queue ?? "tasks";
|
|
5141
|
+
this.target = options.target ?? "localRunner";
|
|
5142
|
+
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5143
|
+
this.pollMs = options.pollMs ?? 1e3;
|
|
5144
|
+
this.maxParallel = options.maxParallel ?? 1;
|
|
5145
|
+
this.scanLimit = options.scanLimit ?? 100;
|
|
5146
|
+
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5147
|
+
this.registry = normalizeRegistry(options.registry);
|
|
5148
|
+
}
|
|
5149
|
+
static init(context, options = {}) {
|
|
5150
|
+
const defs = {
|
|
5151
|
+
table: "string default tasks",
|
|
5152
|
+
target: "string default localRunner",
|
|
5153
|
+
recreateTaskTables: "boolean default false",
|
|
5154
|
+
pollMs: "number default 1000",
|
|
5155
|
+
maxParallel: "number default 1",
|
|
5156
|
+
scanLimit: "number default 100",
|
|
5157
|
+
allowedTasks: "string"
|
|
5158
|
+
};
|
|
5159
|
+
const discovered = context.params.getAllForModule(defs);
|
|
5160
|
+
const resolved = {
|
|
5161
|
+
queue: discovered.table,
|
|
5162
|
+
target: discovered.target,
|
|
5163
|
+
recreateTaskTables: discovered.recreateTaskTables,
|
|
5164
|
+
pollMs: discovered.pollMs,
|
|
5165
|
+
maxParallel: discovered.maxParallel,
|
|
5166
|
+
scanLimit: discovered.scanLimit,
|
|
5167
|
+
allowedTasks: discovered.allowedTasks,
|
|
5168
|
+
...options
|
|
5169
|
+
};
|
|
5170
|
+
return new _TasksManager(context, resolved);
|
|
5171
|
+
}
|
|
5172
|
+
async ensureTaskTables(options = {}) {
|
|
5173
|
+
await ensureTaskTables(this.context, {
|
|
5174
|
+
queue: this.queue,
|
|
5175
|
+
recreate: options.recreate ?? this.recreateTaskTables
|
|
5176
|
+
});
|
|
5177
|
+
}
|
|
5178
|
+
async runTasksLoop(options = {}) {
|
|
5179
|
+
await runTasksLoop(this.context, {
|
|
5180
|
+
queue: options.queue ?? this.queue,
|
|
5181
|
+
target: options.target ?? this.target,
|
|
5182
|
+
pollMs: options.pollMs ?? this.pollMs,
|
|
5183
|
+
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5184
|
+
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5185
|
+
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5186
|
+
registry: options.registry ?? this.registry
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
};
|
|
3864
5190
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3865
5191
|
0 && (module.exports = {
|
|
3866
5192
|
Args,
|
|
@@ -3884,8 +5210,18 @@ function setupContext(opts = {}) {
|
|
|
3884
5210
|
ScreenFooter,
|
|
3885
5211
|
ScreenRow,
|
|
3886
5212
|
ScreenTitle,
|
|
5213
|
+
TaskMaster,
|
|
5214
|
+
TaskPing,
|
|
5215
|
+
TaskSampleProcess,
|
|
5216
|
+
TaskShellCommand,
|
|
5217
|
+
TaskStopRunner,
|
|
5218
|
+
TaskSumAB,
|
|
5219
|
+
TaskSystemInfo,
|
|
5220
|
+
TasksManager,
|
|
5221
|
+
TasksRegistry,
|
|
3887
5222
|
Text,
|
|
3888
5223
|
TextBlock,
|
|
5224
|
+
appendTaskIpcLog,
|
|
3889
5225
|
buildBreadcrumb,
|
|
3890
5226
|
buildDetailBreadcrumb,
|
|
3891
5227
|
buildFooter,
|
|
@@ -3893,8 +5229,11 @@ function setupContext(opts = {}) {
|
|
|
3893
5229
|
dbFindAndConnect,
|
|
3894
5230
|
dbInit,
|
|
3895
5231
|
defaultFileSynopsisFunction,
|
|
5232
|
+
defaultTasksRegistry,
|
|
3896
5233
|
defaultVersionSynopsisFunction,
|
|
3897
|
-
|
|
5234
|
+
enqueueStopTask,
|
|
5235
|
+
enqueueTask,
|
|
5236
|
+
ensureTaskTables,
|
|
3898
5237
|
getArgsInstance,
|
|
3899
5238
|
h,
|
|
3900
5239
|
joiEdateType,
|
|
@@ -3903,6 +5242,9 @@ function setupContext(opts = {}) {
|
|
|
3903
5242
|
listTables,
|
|
3904
5243
|
load,
|
|
3905
5244
|
organizeFooterMessages,
|
|
5245
|
+
queueToTableNames,
|
|
5246
|
+
runNodeTaskScript,
|
|
5247
|
+
runTasksLoop,
|
|
3906
5248
|
setupContext,
|
|
3907
5249
|
showListScreen,
|
|
3908
5250
|
showMenuScreen,
|
|
@@ -3910,11 +5252,13 @@ function setupContext(opts = {}) {
|
|
|
3910
5252
|
showMultiColumnListWithPreviewScreen,
|
|
3911
5253
|
showScreen,
|
|
3912
5254
|
showWordGridScreen,
|
|
5255
|
+
updateTaskProgress,
|
|
3913
5256
|
useCallback,
|
|
3914
5257
|
useEffect,
|
|
3915
5258
|
useInput,
|
|
3916
5259
|
useMemo,
|
|
3917
5260
|
useRef,
|
|
3918
|
-
useState
|
|
5261
|
+
useState,
|
|
5262
|
+
waitForTaskResult
|
|
3919
5263
|
});
|
|
3920
5264
|
//# sourceMappingURL=index.cjs.map
|