@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.js
CHANGED
|
@@ -1073,20 +1073,31 @@ var Args = class _Args {
|
|
|
1073
1073
|
configValues = {};
|
|
1074
1074
|
configsLoaded = [];
|
|
1075
1075
|
env = "local";
|
|
1076
|
-
constructor(
|
|
1076
|
+
constructor(contextOrConfig = {}, config2) {
|
|
1077
|
+
const hasContext = config2 !== void 0;
|
|
1078
|
+
const configToUse = hasContext ? config2 ?? {} : contextOrConfig ?? {};
|
|
1079
|
+
const context = hasContext ? contextOrConfig : void 0;
|
|
1077
1080
|
this.aliases = {};
|
|
1078
1081
|
this.overrides = {};
|
|
1079
1082
|
this.defaults = {};
|
|
1080
1083
|
this.prefixes = ["not", "no"];
|
|
1081
|
-
if (Object.keys(
|
|
1082
|
-
this.configure(
|
|
1084
|
+
if (Object.keys(configToUse).length > 0) {
|
|
1085
|
+
this.configure(configToUse);
|
|
1083
1086
|
}
|
|
1084
|
-
const args =
|
|
1087
|
+
const args = configToUse.args || process.argv.slice(2);
|
|
1085
1088
|
this.parseArgs(args);
|
|
1086
1089
|
this.env = this.get("env")?.toLowerCase() || "local";
|
|
1087
1090
|
this.loadDotEnv();
|
|
1088
1091
|
this.loadConfigFiles();
|
|
1089
1092
|
this.checkConflicts();
|
|
1093
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1094
|
+
context.registerCleanup((ctx) => {
|
|
1095
|
+
const unusedArgs = ctx.args.getUnused();
|
|
1096
|
+
if (unusedArgs.length > 0) {
|
|
1097
|
+
ctx.logger.warn("Unused CLI args:", unusedArgs.join(", "));
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1090
1101
|
}
|
|
1091
1102
|
/**
|
|
1092
1103
|
* Configure Args options
|
|
@@ -1108,12 +1119,15 @@ var Args = class _Args {
|
|
|
1108
1119
|
}
|
|
1109
1120
|
}
|
|
1110
1121
|
/**
|
|
1111
|
-
* Initialize Args instance
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1122
|
+
* Initialize Args instance.
|
|
1123
|
+
* Args.init(context, config) when used from init/setup: context has registerCleanup, Args registers unused-args cleanup.
|
|
1124
|
+
* Args.init(config) for standalone use (no cleanup).
|
|
1114
1125
|
*/
|
|
1115
|
-
static init(config2
|
|
1116
|
-
|
|
1126
|
+
static init(contextOrConfig, config2) {
|
|
1127
|
+
if (config2 !== void 0) {
|
|
1128
|
+
return new _Args(contextOrConfig, config2);
|
|
1129
|
+
}
|
|
1130
|
+
return new _Args(contextOrConfig ?? {});
|
|
1117
1131
|
}
|
|
1118
1132
|
/**
|
|
1119
1133
|
* Parse command line arguments
|
|
@@ -1293,6 +1307,32 @@ var Args = class _Args {
|
|
|
1293
1307
|
}
|
|
1294
1308
|
return void 0;
|
|
1295
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Return which layer provided the value for get(key): overrides, cli, config, env, or default.
|
|
1312
|
+
* Does not add key to usedKeys. Use after get(key) when you need the origin.
|
|
1313
|
+
*/
|
|
1314
|
+
getSource(key) {
|
|
1315
|
+
const resolvedKey = this.aliases[key] || key;
|
|
1316
|
+
const lcKey = resolvedKey.toLowerCase();
|
|
1317
|
+
const overrideKey = Object.keys(this.overrides).find((k) => k.toLowerCase() === lcKey);
|
|
1318
|
+
if (overrideKey !== void 0) return "overrides";
|
|
1319
|
+
const lcKeyWithEnv = `${lcKey}${this.env ? `_${this.env.toLowerCase()}` : ""}`;
|
|
1320
|
+
if (this.env && this.args[lcKeyWithEnv] !== void 0) return "cli";
|
|
1321
|
+
if (this.args[lcKey] !== void 0) return "cli";
|
|
1322
|
+
const configKey = Object.keys(this.configValues).find((k) => k.toLowerCase() === lcKey);
|
|
1323
|
+
if (configKey !== void 0) return "config";
|
|
1324
|
+
const envKey = this.toEnvKey(resolvedKey);
|
|
1325
|
+
const envKeyWithEnv = `${envKey}${this.env ? `_${this.env.toUpperCase()}` : ""}`;
|
|
1326
|
+
const envSpecificKey = Object.keys(process.env).find((k) => this.env && k.toUpperCase() === envKeyWithEnv);
|
|
1327
|
+
const envKeyFound = Object.keys(process.env).find((k) => k.toUpperCase() === envKey);
|
|
1328
|
+
const envKeyAlt = envKey.replace(/_([0-9])/g, "$1");
|
|
1329
|
+
const envKeyAltFound = !envKeyFound ? Object.keys(process.env).find((k) => k.toUpperCase() === envKeyAlt) : null;
|
|
1330
|
+
if (envSpecificKey || envKeyFound || envKeyAltFound) return "env";
|
|
1331
|
+
const defaultKey = Object.keys(this.defaults).find((k) => k.toLowerCase() === lcKey);
|
|
1332
|
+
if (defaultKey !== void 0) return "default";
|
|
1333
|
+
if (lcKey === "env" && process.env.NODE_ENV !== void 0) return "env";
|
|
1334
|
+
return void 0;
|
|
1335
|
+
}
|
|
1296
1336
|
/**
|
|
1297
1337
|
* Set a value (for testing/internal use)
|
|
1298
1338
|
*/
|
|
@@ -1518,6 +1558,12 @@ var ParamError = class extends FrameworkError {
|
|
|
1518
1558
|
this.name = "ParamError";
|
|
1519
1559
|
}
|
|
1520
1560
|
};
|
|
1561
|
+
var FileDatabaseError = class extends FrameworkError {
|
|
1562
|
+
constructor(message) {
|
|
1563
|
+
super(message);
|
|
1564
|
+
this.name = "FileDatabaseError";
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1521
1567
|
|
|
1522
1568
|
// src/params/custom-types.ts
|
|
1523
1569
|
var joiEdateType = (value, helpers) => {
|
|
@@ -1643,17 +1689,53 @@ var Params = class _Params {
|
|
|
1643
1689
|
context;
|
|
1644
1690
|
// Partial context during initialization
|
|
1645
1691
|
params = {};
|
|
1692
|
+
paramSources = {};
|
|
1646
1693
|
definitions = {};
|
|
1647
1694
|
args;
|
|
1648
1695
|
paramSetters = [];
|
|
1649
1696
|
paramGetters = [];
|
|
1650
1697
|
trackedParams = [];
|
|
1698
|
+
_currentModule = "script";
|
|
1699
|
+
/** Resolved early in constructor so cleanup does not read params lazily */
|
|
1700
|
+
_showUsedParams = false;
|
|
1651
1701
|
constructor(context, options = {}) {
|
|
1652
1702
|
this.context = context;
|
|
1653
1703
|
this.args = context.args;
|
|
1654
1704
|
if (Object.keys(options).length > 0) {
|
|
1655
1705
|
this.configure(options);
|
|
1656
1706
|
}
|
|
1707
|
+
this._showUsedParams = this.get("showUsedParams", "boolean default false");
|
|
1708
|
+
if (context && typeof context.registerCleanup === "function") {
|
|
1709
|
+
context.registerCleanup((ctx) => {
|
|
1710
|
+
if (!ctx.params.getShowUsedParams()) return;
|
|
1711
|
+
const byModule = ctx.params.getFiguredByModule();
|
|
1712
|
+
const modules = Object.keys(byModule).sort();
|
|
1713
|
+
if (modules.length === 0) return;
|
|
1714
|
+
const logger = ctx.logger;
|
|
1715
|
+
logger.debug("[Params]: list of used params:");
|
|
1716
|
+
if (typeof logger.highlight !== "function") {
|
|
1717
|
+
for (const mod of modules) {
|
|
1718
|
+
logger.debug(` [${mod}]`);
|
|
1719
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1720
|
+
logger.debug(` ${key}: ${JSON.stringify(entry.value)} (${entry.source})`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
for (const mod of modules) {
|
|
1726
|
+
logger.debug(` [${mod}]`);
|
|
1727
|
+
for (const [key, entry] of Object.entries(byModule[mod])) {
|
|
1728
|
+
const valueStr = JSON.stringify(entry.value);
|
|
1729
|
+
const display = entry.source === "default" ? valueStr : logger.highlight(valueStr);
|
|
1730
|
+
logger.debug(` ${key}: ${display} (${entry.source})`);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/** Whether --showUsedParams was requested (resolved in constructor). */
|
|
1737
|
+
getShowUsedParams() {
|
|
1738
|
+
return this._showUsedParams;
|
|
1657
1739
|
}
|
|
1658
1740
|
/**
|
|
1659
1741
|
* Configure parameters
|
|
@@ -1672,14 +1754,15 @@ var Params = class _Params {
|
|
|
1672
1754
|
return new _Params(context, options || {});
|
|
1673
1755
|
}
|
|
1674
1756
|
/**
|
|
1675
|
-
* Track a parameter request for --stopAfter=init
|
|
1757
|
+
* Track a parameter request for --stopAfter=init and --showUsedParams
|
|
1676
1758
|
*/
|
|
1677
|
-
trackParam(key, definition, value, source) {
|
|
1759
|
+
trackParam(key, definition, value, source, moduleName) {
|
|
1678
1760
|
this.trackedParams.push({
|
|
1679
1761
|
key,
|
|
1680
1762
|
definition,
|
|
1681
1763
|
value,
|
|
1682
|
-
source
|
|
1764
|
+
source,
|
|
1765
|
+
module: moduleName ?? this._currentModule
|
|
1683
1766
|
});
|
|
1684
1767
|
}
|
|
1685
1768
|
/**
|
|
@@ -1689,7 +1772,7 @@ var Params = class _Params {
|
|
|
1689
1772
|
return [...this.trackedParams];
|
|
1690
1773
|
}
|
|
1691
1774
|
/**
|
|
1692
|
-
* Get all figured parameters as a record
|
|
1775
|
+
* Get all figured parameters as a record (flat, last occurrence per key)
|
|
1693
1776
|
* Returns all parameters that were collected during initialization,
|
|
1694
1777
|
* whether from CLI args, options, or defaults
|
|
1695
1778
|
*/
|
|
@@ -1703,6 +1786,19 @@ var Params = class _Params {
|
|
|
1703
1786
|
}
|
|
1704
1787
|
return result;
|
|
1705
1788
|
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Get figured parameters grouped by module name.
|
|
1791
|
+
* Same param can appear in multiple modules (e.g. source, resource).
|
|
1792
|
+
*/
|
|
1793
|
+
getFiguredByModule() {
|
|
1794
|
+
const byModule = {};
|
|
1795
|
+
for (const param of this.trackedParams) {
|
|
1796
|
+
const mod = param.module;
|
|
1797
|
+
if (!byModule[mod]) byModule[mod] = {};
|
|
1798
|
+
byModule[mod][param.key] = { value: param.value, source: param.source };
|
|
1799
|
+
}
|
|
1800
|
+
return byModule;
|
|
1801
|
+
}
|
|
1706
1802
|
/**
|
|
1707
1803
|
* Clear tracked parameters
|
|
1708
1804
|
*/
|
|
@@ -1821,14 +1917,19 @@ var Params = class _Params {
|
|
|
1821
1917
|
source = "options";
|
|
1822
1918
|
} else if (valFromArgs !== void 0 && valFromArgs !== null) {
|
|
1823
1919
|
value = this.validate(key, valFromArgs, def);
|
|
1824
|
-
|
|
1920
|
+
const argsSource = this.args.getSource?.(key);
|
|
1921
|
+
if (argsSource === "overrides") source = "options";
|
|
1922
|
+
else if (argsSource === "cli" || argsSource === "env" || argsSource === "config") source = argsSource;
|
|
1923
|
+
else if (argsSource === "default") source = "default";
|
|
1924
|
+
else source = "cli";
|
|
1825
1925
|
} else if (valFromParams !== void 0 && valFromParams !== null) {
|
|
1826
1926
|
value = this.validate(key, valFromParams, def);
|
|
1827
|
-
source = "options";
|
|
1927
|
+
source = this.paramSources[key] ?? "options";
|
|
1828
1928
|
} else {
|
|
1829
1929
|
value = this.validate(key, void 0, def);
|
|
1830
1930
|
source = "default";
|
|
1831
1931
|
}
|
|
1932
|
+
this.paramSources[key] = source;
|
|
1832
1933
|
this.trackParam(key, definition || "string", value, source);
|
|
1833
1934
|
if (value !== void 0 && def.values && !def.values.includes(value)) {
|
|
1834
1935
|
throw new ParamError(`key ${key} should be one of ${def.values}`);
|
|
@@ -1849,19 +1950,63 @@ var Params = class _Params {
|
|
|
1849
1950
|
}
|
|
1850
1951
|
}
|
|
1851
1952
|
/**
|
|
1852
|
-
* Get all parameters from definitions
|
|
1853
|
-
* Processes
|
|
1953
|
+
* Get all parameters from definitions (main script).
|
|
1954
|
+
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
1854
1955
|
*/
|
|
1855
1956
|
getAll(defs) {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1957
|
+
return this.getAllForModule("script", defs);
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Get all parameters from definitions for a given module name.
|
|
1961
|
+
* Figured params are grouped by module when using --showUsedParams.
|
|
1962
|
+
* Processes parameters left-to-right to support cross-parameter references.
|
|
1963
|
+
* If moduleName is omitted, it is inferred from the caller's file path (directory name under src/).
|
|
1964
|
+
*/
|
|
1965
|
+
getAllForModule(moduleNameOrDefs, defs) {
|
|
1966
|
+
let moduleName;
|
|
1967
|
+
let definitions;
|
|
1968
|
+
if (defs !== void 0) {
|
|
1969
|
+
moduleName = moduleNameOrDefs;
|
|
1970
|
+
definitions = defs;
|
|
1971
|
+
} else {
|
|
1972
|
+
definitions = moduleNameOrDefs;
|
|
1973
|
+
moduleName = this._inferModuleNameFromStack();
|
|
1974
|
+
}
|
|
1975
|
+
const prev = this._currentModule;
|
|
1976
|
+
this._currentModule = moduleName;
|
|
1977
|
+
try {
|
|
1978
|
+
const res = {};
|
|
1979
|
+
for (const [k, def] of Object.entries(definitions)) {
|
|
1980
|
+
const value = this.get(k, def);
|
|
1981
|
+
res[k] = value;
|
|
1982
|
+
if (value !== void 0) {
|
|
1983
|
+
this.params[k] = value;
|
|
1984
|
+
}
|
|
1862
1985
|
}
|
|
1986
|
+
return res;
|
|
1987
|
+
} finally {
|
|
1988
|
+
this._currentModule = prev;
|
|
1863
1989
|
}
|
|
1864
|
-
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
1993
|
+
*/
|
|
1994
|
+
_inferModuleNameFromStack() {
|
|
1995
|
+
const stack = new Error().stack;
|
|
1996
|
+
if (!stack) return "script";
|
|
1997
|
+
const lines = stack.split("\n");
|
|
1998
|
+
const paramsIndexPath = "params" + (typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/") + "index.";
|
|
1999
|
+
for (const line of lines) {
|
|
2000
|
+
const parenMatch = line.match(/\(([^)]+)\)/);
|
|
2001
|
+
if (!parenMatch) continue;
|
|
2002
|
+
const parts = parenMatch[1].split(":");
|
|
2003
|
+
if (parts.length < 3) continue;
|
|
2004
|
+
const path4 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2005
|
+
if (!path4 || path4.includes(paramsIndexPath)) continue;
|
|
2006
|
+
const srcMatch = path4.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2007
|
+
if (srcMatch) return srcMatch[1];
|
|
2008
|
+
}
|
|
2009
|
+
return "script";
|
|
1865
2010
|
}
|
|
1866
2011
|
/**
|
|
1867
2012
|
* Run all registered getters for a key
|
|
@@ -2083,13 +2228,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2083
2228
|
}
|
|
2084
2229
|
|
|
2085
2230
|
// src/filedatabase/index.ts
|
|
2086
|
-
var
|
|
2087
|
-
constructor(message) {
|
|
2088
|
-
super(message);
|
|
2089
|
-
this.name = "FileDatabaseError";
|
|
2090
|
-
}
|
|
2091
|
-
};
|
|
2092
|
-
var FileDatabase = class {
|
|
2231
|
+
var FileDatabase = class _FileDatabase {
|
|
2093
2232
|
basePath;
|
|
2094
2233
|
namespace;
|
|
2095
2234
|
tableName = null;
|
|
@@ -2126,18 +2265,8 @@ var FileDatabase = class {
|
|
|
2126
2265
|
maxVersions: "number default 5",
|
|
2127
2266
|
pageSize: "number default 5000"
|
|
2128
2267
|
};
|
|
2129
|
-
const
|
|
2130
|
-
config2 = {
|
|
2131
|
-
basePath: opts.basePath ?? paramsConfig.basePath,
|
|
2132
|
-
namespace: opts.namespace ?? paramsConfig.namespace,
|
|
2133
|
-
tableName: opts.tableName ?? paramsConfig.tableName ?? null,
|
|
2134
|
-
versioned: opts.versioned ?? true,
|
|
2135
|
-
maxVersions: opts.maxVersions ?? paramsConfig.maxVersions,
|
|
2136
|
-
pageSize: opts.pageSize ?? paramsConfig.pageSize,
|
|
2137
|
-
useMetadata: opts.useMetadata ?? true,
|
|
2138
|
-
freeSpaceThreshold: opts.freeSpaceThreshold ?? 100 * 1024 * 1024,
|
|
2139
|
-
logger: context.logger
|
|
2140
|
-
};
|
|
2268
|
+
const discovered = context.params.getAllForModule(defs);
|
|
2269
|
+
config2 = { ...discovered, ...opts, logger: context.logger };
|
|
2141
2270
|
} else {
|
|
2142
2271
|
config2 = contextOrConfig;
|
|
2143
2272
|
}
|
|
@@ -2155,6 +2284,13 @@ var FileDatabase = class {
|
|
|
2155
2284
|
this.logger = config2.logger || console;
|
|
2156
2285
|
this.metadata = this.getDefaultMetadata();
|
|
2157
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Initialize FileDatabase from context and options.
|
|
2289
|
+
* Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
|
|
2290
|
+
*/
|
|
2291
|
+
static init(context, options) {
|
|
2292
|
+
return new _FileDatabase(context, options ?? {});
|
|
2293
|
+
}
|
|
2158
2294
|
/**
|
|
2159
2295
|
* Get default metadata structure
|
|
2160
2296
|
*/
|
|
@@ -2222,7 +2358,7 @@ var FileDatabase = class {
|
|
|
2222
2358
|
const versions = await this.getVersions();
|
|
2223
2359
|
while (versions.length > this.maxVersions) {
|
|
2224
2360
|
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
2225
|
-
this.logger.
|
|
2361
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
2226
2362
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
2227
2363
|
}
|
|
2228
2364
|
return versionName;
|
|
@@ -2500,7 +2636,7 @@ var FileDatabase = class {
|
|
|
2500
2636
|
};
|
|
2501
2637
|
this.metadata.files.push(fileEntry);
|
|
2502
2638
|
this.lastFileData = null;
|
|
2503
|
-
this.logger.
|
|
2639
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
2504
2640
|
}
|
|
2505
2641
|
/**
|
|
2506
2642
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -2533,7 +2669,7 @@ var FileDatabase = class {
|
|
|
2533
2669
|
const filesBeforeCreate = this.metadata.files.length;
|
|
2534
2670
|
this.makeNewFile();
|
|
2535
2671
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
2536
|
-
this.logger.
|
|
2672
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
2537
2673
|
} else if (this.metadata.files.length === 0) {
|
|
2538
2674
|
this.makeNewFile();
|
|
2539
2675
|
}
|
|
@@ -2582,7 +2718,7 @@ var FileDatabase = class {
|
|
|
2582
2718
|
dataLeftOver = null;
|
|
2583
2719
|
}
|
|
2584
2720
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
2585
|
-
this.logger.
|
|
2721
|
+
this.logger.silly?.(
|
|
2586
2722
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
2587
2723
|
);
|
|
2588
2724
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -2637,7 +2773,7 @@ var FileDatabase = class {
|
|
|
2637
2773
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2638
2774
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
2639
2775
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
2640
|
-
this.logger.
|
|
2776
|
+
this.logger.silly?.(
|
|
2641
2777
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
2642
2778
|
);
|
|
2643
2779
|
}
|
|
@@ -2661,7 +2797,7 @@ var FileDatabase = class {
|
|
|
2661
2797
|
}
|
|
2662
2798
|
try {
|
|
2663
2799
|
await fs3.promises.writeFile(filePath, serializedData, "utf8");
|
|
2664
|
-
this.logger.
|
|
2800
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
2665
2801
|
} catch (error) {
|
|
2666
2802
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
2667
2803
|
}
|
|
@@ -2743,7 +2879,8 @@ var FileDatabase = class {
|
|
|
2743
2879
|
this.useMetadata = format.hasMetadata;
|
|
2744
2880
|
}
|
|
2745
2881
|
if (this.useMetadata) {
|
|
2746
|
-
const
|
|
2882
|
+
const destPath = this.getDestinationPath();
|
|
2883
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
2747
2884
|
if (fs3.existsSync(metadataPath)) {
|
|
2748
2885
|
try {
|
|
2749
2886
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -2757,7 +2894,9 @@ var FileDatabase = class {
|
|
|
2757
2894
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2758
2895
|
}
|
|
2759
2896
|
} else {
|
|
2760
|
-
throw new FileDatabaseError(
|
|
2897
|
+
throw new FileDatabaseError(
|
|
2898
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
2899
|
+
);
|
|
2761
2900
|
}
|
|
2762
2901
|
} else {
|
|
2763
2902
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -2774,6 +2913,13 @@ var FileDatabase = class {
|
|
|
2774
2913
|
* Write data to the file database
|
|
2775
2914
|
*/
|
|
2776
2915
|
async write(data, options = {}) {
|
|
2916
|
+
if (options.filename) {
|
|
2917
|
+
const destPath2 = this.getDestinationPath();
|
|
2918
|
+
await ensurePath(destPath2);
|
|
2919
|
+
const filePath = path3.join(destPath2, options.filename);
|
|
2920
|
+
await this.safeWrite(filePath, data);
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2777
2923
|
if (options.forceNewVersion && !this.versioned) {
|
|
2778
2924
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2779
2925
|
}
|
|
@@ -2797,17 +2943,17 @@ var FileDatabase = class {
|
|
|
2797
2943
|
});
|
|
2798
2944
|
if (matches) {
|
|
2799
2945
|
targetFileIndex = i;
|
|
2800
|
-
this.logger.
|
|
2946
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2801
2947
|
break;
|
|
2802
2948
|
} else {
|
|
2803
|
-
this.logger.
|
|
2949
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2804
2950
|
}
|
|
2805
2951
|
}
|
|
2806
2952
|
if (targetFileIndex === null) {
|
|
2807
|
-
this.logger.
|
|
2953
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
2808
2954
|
}
|
|
2809
2955
|
} else {
|
|
2810
|
-
this.logger.
|
|
2956
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
2811
2957
|
}
|
|
2812
2958
|
if (targetFileIndex !== null) {
|
|
2813
2959
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -2836,7 +2982,17 @@ var FileDatabase = class {
|
|
|
2836
2982
|
* Read data from the file database
|
|
2837
2983
|
*/
|
|
2838
2984
|
async read(options = {}) {
|
|
2839
|
-
const { version, nextPage = false, pageSize } = options;
|
|
2985
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
2986
|
+
if (filename) {
|
|
2987
|
+
const destPath = this.getDestinationPath(version);
|
|
2988
|
+
const filePath = path3.join(destPath, filename);
|
|
2989
|
+
try {
|
|
2990
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
2991
|
+
return JSON.parse(rawData);
|
|
2992
|
+
} catch (error) {
|
|
2993
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2840
2996
|
await this.prepare({ read: true, version });
|
|
2841
2997
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
2842
2998
|
if (isNonPaginatedData) {
|
|
@@ -2917,6 +3073,67 @@ var FileDatabase = class {
|
|
|
2917
3073
|
this.currentRecord = 0;
|
|
2918
3074
|
this.hasReadFirstPage = false;
|
|
2919
3075
|
}
|
|
3076
|
+
/**
|
|
3077
|
+
* List filenames in the table directory.
|
|
3078
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
3079
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
3080
|
+
*/
|
|
3081
|
+
async listFilenames() {
|
|
3082
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3083
|
+
try {
|
|
3084
|
+
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
3085
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
3086
|
+
} catch (err) {
|
|
3087
|
+
if (err?.code === "ENOENT") return [];
|
|
3088
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
/**
|
|
3092
|
+
* Remove a file from the table directory (catalog mode).
|
|
3093
|
+
* Use with listFilenames() to manage individual files.
|
|
3094
|
+
*/
|
|
3095
|
+
async removeFile(filename) {
|
|
3096
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3097
|
+
const filePath = path3.join(destPath, filename);
|
|
3098
|
+
try {
|
|
3099
|
+
await fs3.promises.unlink(filePath);
|
|
3100
|
+
} catch (err) {
|
|
3101
|
+
if (err?.code === "ENOENT") return;
|
|
3102
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
/**
|
|
3106
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
3107
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
3108
|
+
*/
|
|
3109
|
+
async removeFileEntry(filename) {
|
|
3110
|
+
if (this.versioned) {
|
|
3111
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
3112
|
+
}
|
|
3113
|
+
await this.prepare({ read: true });
|
|
3114
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
3115
|
+
if (idx === -1) {
|
|
3116
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
3117
|
+
}
|
|
3118
|
+
const entry = this.metadata.files[idx];
|
|
3119
|
+
const recordsCount = entry.recordsCount || 0;
|
|
3120
|
+
this.metadata.files.splice(idx, 1);
|
|
3121
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
3122
|
+
const destPath = this.getDestinationPath();
|
|
3123
|
+
const filePath = path3.join(destPath, filename);
|
|
3124
|
+
try {
|
|
3125
|
+
await fs3.promises.unlink(filePath);
|
|
3126
|
+
} catch (err) {
|
|
3127
|
+
if (err?.code === "ENOENT") {
|
|
3128
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
3129
|
+
} else {
|
|
3130
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
if (this.useMetadata) {
|
|
3134
|
+
await this.saveVersionMetadata(this.metadata);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
2920
3137
|
/**
|
|
2921
3138
|
* Set file-level synopsis calculation function
|
|
2922
3139
|
*/
|
|
@@ -3020,9 +3237,6 @@ function listSources(basePath) {
|
|
|
3020
3237
|
return [];
|
|
3021
3238
|
}
|
|
3022
3239
|
}
|
|
3023
|
-
function fileDatabaseInit(context, options = {}) {
|
|
3024
|
-
return new FileDatabase(context, options);
|
|
3025
|
-
}
|
|
3026
3240
|
|
|
3027
3241
|
// src/db/index.ts
|
|
3028
3242
|
import knex from "knex";
|
|
@@ -3321,6 +3535,13 @@ var Db = class {
|
|
|
3321
3535
|
isConnectedToDb() {
|
|
3322
3536
|
return this.isConnected && this.knexInstance !== null;
|
|
3323
3537
|
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
3540
|
+
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
3541
|
+
*/
|
|
3542
|
+
static async init(context, dbNameOrConnectionString) {
|
|
3543
|
+
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
3544
|
+
}
|
|
3324
3545
|
};
|
|
3325
3546
|
function capitalizeFirstLetter(str) {
|
|
3326
3547
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
@@ -3334,7 +3555,7 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3334
3555
|
acquireConnectionTimeout: "number default 10000",
|
|
3335
3556
|
sslRejectUnauthorized: "boolean default false"
|
|
3336
3557
|
};
|
|
3337
|
-
const paramsConfig = context.params.
|
|
3558
|
+
const paramsConfig = context.params.getAllForModule(defs);
|
|
3338
3559
|
const config2 = {
|
|
3339
3560
|
connectionString,
|
|
3340
3561
|
name: paramsConfig.name || name || "default",
|
|
@@ -3384,7 +3605,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
|
3384
3605
|
dbConnectionString: "string",
|
|
3385
3606
|
dbProfile: "boolean default false"
|
|
3386
3607
|
};
|
|
3387
|
-
const paramsConfig = context.params.
|
|
3608
|
+
const paramsConfig = context.params.getAllForModule(defs);
|
|
3388
3609
|
dbName = paramsConfig.dbName;
|
|
3389
3610
|
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3390
3611
|
dbProfile = paramsConfig.dbProfile;
|
|
@@ -3446,6 +3667,7 @@ var ALL_LEVELS = [
|
|
|
3446
3667
|
"response",
|
|
3447
3668
|
"progress"
|
|
3448
3669
|
];
|
|
3670
|
+
var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
|
|
3449
3671
|
var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
|
|
3450
3672
|
var LEVEL_COLORS = {
|
|
3451
3673
|
error: chalk.red.bold,
|
|
@@ -3476,8 +3698,7 @@ var Logger = class _Logger {
|
|
|
3476
3698
|
this.updateTransport();
|
|
3477
3699
|
}
|
|
3478
3700
|
/**
|
|
3479
|
-
* Configure logger options
|
|
3480
|
-
* Only parameters present in options are updated
|
|
3701
|
+
* Configure logger options. Accepts both LoggerOptions shape and flat param names (levels string, progressWithTimes, progressThrottleMs).
|
|
3481
3702
|
*/
|
|
3482
3703
|
configure(options) {
|
|
3483
3704
|
if (options.mode !== void 0) {
|
|
@@ -3487,32 +3708,24 @@ var Logger = class _Logger {
|
|
|
3487
3708
|
this.options.route = options.route;
|
|
3488
3709
|
this.updateTransport();
|
|
3489
3710
|
}
|
|
3490
|
-
if (options.prefix !== void 0)
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
if (options.
|
|
3494
|
-
this.options.silent = options.silent;
|
|
3495
|
-
}
|
|
3496
|
-
if (options.showLevel !== void 0) {
|
|
3497
|
-
this.options.showLevel = options.showLevel;
|
|
3498
|
-
}
|
|
3499
|
-
if (options.timestamp !== void 0) {
|
|
3500
|
-
this.options.timestamp = options.timestamp;
|
|
3501
|
-
}
|
|
3711
|
+
if (options.prefix !== void 0) this.options.prefix = options.prefix;
|
|
3712
|
+
if (options.silent !== void 0) this.options.silent = options.silent;
|
|
3713
|
+
if (options.showLevel !== void 0) this.options.showLevel = options.showLevel;
|
|
3714
|
+
if (options.timestamp !== void 0) this.options.timestamp = options.timestamp;
|
|
3502
3715
|
if (options.levels !== void 0) {
|
|
3503
|
-
|
|
3716
|
+
const levels = typeof options.levels === "string" ? options.levels.split(",") : options.levels;
|
|
3717
|
+
this.options.levels = this.normalizeLevels(levels);
|
|
3504
3718
|
}
|
|
3505
3719
|
if (options.progress !== void 0) {
|
|
3506
|
-
if (options.progress.withTimes !== void 0)
|
|
3507
|
-
|
|
3508
|
-
}
|
|
3509
|
-
if (options.progress.throttleMs !== void 0) {
|
|
3510
|
-
this.options.progressThrottle = options.progress.throttleMs;
|
|
3511
|
-
}
|
|
3720
|
+
if (options.progress.withTimes !== void 0) this.options.progressTimes = options.progress.withTimes;
|
|
3721
|
+
if (options.progress.throttleMs !== void 0) this.options.progressThrottle = options.progress.throttleMs;
|
|
3512
3722
|
}
|
|
3723
|
+
const flat = options;
|
|
3724
|
+
if (flat.progressWithTimes !== void 0) this.options.progressTimes = flat.progressWithTimes;
|
|
3725
|
+
if (flat.progressThrottleMs !== void 0) this.options.progressThrottle = flat.progressThrottleMs;
|
|
3513
3726
|
}
|
|
3514
3727
|
/**
|
|
3515
|
-
* Initialize logger from context and CLI parameters
|
|
3728
|
+
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
3516
3729
|
*/
|
|
3517
3730
|
static init(context, options) {
|
|
3518
3731
|
const paramDefs = {
|
|
@@ -3526,20 +3739,8 @@ var Logger = class _Logger {
|
|
|
3526
3739
|
progressWithTimes: "boolean default false",
|
|
3527
3740
|
progressThrottleMs: "number"
|
|
3528
3741
|
};
|
|
3529
|
-
const
|
|
3530
|
-
const config2 = {
|
|
3531
|
-
mode: options?.mode ?? cliParams.mode,
|
|
3532
|
-
route: options?.route ?? cliParams.route,
|
|
3533
|
-
prefix: options?.prefix ?? cliParams.prefix,
|
|
3534
|
-
silent: options?.silent ?? cliParams.silent,
|
|
3535
|
-
showLevel: options?.showLevel ?? cliParams.showLevel,
|
|
3536
|
-
timestamp: options?.timestamp ?? cliParams.timestamp,
|
|
3537
|
-
levels: options?.levels ?? (cliParams.levels ? cliParams.levels.split(",") : void 0),
|
|
3538
|
-
progress: options?.progress ?? {
|
|
3539
|
-
withTimes: cliParams.progressWithTimes,
|
|
3540
|
-
throttleMs: cliParams.progressThrottleMs
|
|
3541
|
-
}
|
|
3542
|
-
};
|
|
3742
|
+
const discovered = context.params.getAllForModule(paramDefs);
|
|
3743
|
+
const config2 = { ...discovered, ...options };
|
|
3543
3744
|
const logger = new _Logger(context, config2);
|
|
3544
3745
|
context.logger = logger;
|
|
3545
3746
|
return logger;
|
|
@@ -3552,7 +3753,7 @@ var Logger = class _Logger {
|
|
|
3552
3753
|
silent: false,
|
|
3553
3754
|
showLevel: false,
|
|
3554
3755
|
timestamp: false,
|
|
3555
|
-
levels:
|
|
3756
|
+
levels: DEFAULT_LEVELS,
|
|
3556
3757
|
progressTimes: false,
|
|
3557
3758
|
progressThrottle: void 0
|
|
3558
3759
|
};
|
|
@@ -3566,6 +3767,10 @@ var Logger = class _Logger {
|
|
|
3566
3767
|
}
|
|
3567
3768
|
this.options.mode = mode;
|
|
3568
3769
|
}
|
|
3770
|
+
/** Returns a styled string (bright white) for highlighting; keeps chalk inside logger. */
|
|
3771
|
+
highlight(text) {
|
|
3772
|
+
return chalk.whiteBright(text);
|
|
3773
|
+
}
|
|
3569
3774
|
debug(message, ...chunks) {
|
|
3570
3775
|
this.out({ level: "debug", message, chunks });
|
|
3571
3776
|
}
|
|
@@ -3708,15 +3913,17 @@ var Logger = class _Logger {
|
|
|
3708
3913
|
}
|
|
3709
3914
|
normalizeLevels(levels) {
|
|
3710
3915
|
if (!levels || !levels.length) {
|
|
3711
|
-
return
|
|
3916
|
+
return DEFAULT_LEVELS;
|
|
3712
3917
|
}
|
|
3713
|
-
const
|
|
3714
|
-
const
|
|
3715
|
-
const
|
|
3918
|
+
const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
|
|
3919
|
+
const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
|
|
3920
|
+
const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
|
|
3921
|
+
const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
|
|
3922
|
+
const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
|
|
3716
3923
|
if (unknown.length) {
|
|
3717
3924
|
console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
|
|
3718
3925
|
}
|
|
3719
|
-
const base =
|
|
3926
|
+
const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
|
|
3720
3927
|
return base.filter((level) => !excludes.includes(level));
|
|
3721
3928
|
}
|
|
3722
3929
|
isValidMode(mode) {
|
|
@@ -3741,12 +3948,7 @@ function extractComponentOptions(opts, componentName) {
|
|
|
3741
3948
|
return componentOptions;
|
|
3742
3949
|
}
|
|
3743
3950
|
function setup(opts = {}) {
|
|
3744
|
-
const args = Args.init({
|
|
3745
|
-
overrides: opts.overrides || {},
|
|
3746
|
-
defaults: opts.defaults || {}
|
|
3747
|
-
});
|
|
3748
3951
|
const partialContext = {
|
|
3749
|
-
args,
|
|
3750
3952
|
emitter: new EventEmitter(),
|
|
3751
3953
|
isStop: () => false,
|
|
3752
3954
|
cleanupFunctions: [],
|
|
@@ -3754,6 +3956,11 @@ function setup(opts = {}) {
|
|
|
3754
3956
|
partialContext.cleanupFunctions.push(fn);
|
|
3755
3957
|
}
|
|
3756
3958
|
};
|
|
3959
|
+
const args = Args.init(partialContext, {
|
|
3960
|
+
overrides: opts.overrides || {},
|
|
3961
|
+
defaults: opts.defaults || {}
|
|
3962
|
+
});
|
|
3963
|
+
partialContext.args = args;
|
|
3757
3964
|
const params = Params.init(partialContext, opts.overrides || {});
|
|
3758
3965
|
partialContext.params = params;
|
|
3759
3966
|
const loggerOptions = extractComponentOptions(opts, "logger");
|
|
@@ -3774,6 +3981,1107 @@ function setup(opts = {}) {
|
|
|
3774
3981
|
function setupContext(opts = {}) {
|
|
3775
3982
|
return setup(opts);
|
|
3776
3983
|
}
|
|
3984
|
+
|
|
3985
|
+
// src/utils/core-utils.ts
|
|
3986
|
+
function sleepMs(ms) {
|
|
3987
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3988
|
+
}
|
|
3989
|
+
function toJsonColumn(value) {
|
|
3990
|
+
if (value === void 0 || value === null) return null;
|
|
3991
|
+
return JSON.stringify(value);
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
// src/tasks/taskUtils.ts
|
|
3995
|
+
import { randomUUID } from "crypto";
|
|
3996
|
+
function getDb(context) {
|
|
3997
|
+
const db = context.db;
|
|
3998
|
+
if (!db) {
|
|
3999
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4000
|
+
}
|
|
4001
|
+
return db;
|
|
4002
|
+
}
|
|
4003
|
+
function queueToTableNames(queue) {
|
|
4004
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4005
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4006
|
+
}
|
|
4007
|
+
return {
|
|
4008
|
+
tasksTable: queue,
|
|
4009
|
+
historyTable: `${queue}_history`
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
async function ensureTaskTables(context, options = {}) {
|
|
4013
|
+
const queue = options.queue ?? "tasks";
|
|
4014
|
+
const recreate = options.recreate ?? false;
|
|
4015
|
+
const db = getDb(context);
|
|
4016
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4017
|
+
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4018
|
+
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4019
|
+
if (recreate) {
|
|
4020
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
4021
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
4022
|
+
}
|
|
4023
|
+
if (needsTasks) {
|
|
4024
|
+
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4025
|
+
await db.schema.createTable(tasksTable, (t) => {
|
|
4026
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4027
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4028
|
+
t.timestamp("started_at");
|
|
4029
|
+
t.timestamp("completed_at");
|
|
4030
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4031
|
+
t.text("schedule");
|
|
4032
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4033
|
+
t.text("target").notNullable();
|
|
4034
|
+
t.text("task").notNullable();
|
|
4035
|
+
t.json("params");
|
|
4036
|
+
t.text("opid");
|
|
4037
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4038
|
+
t.text("progress");
|
|
4039
|
+
t.boolean("success");
|
|
4040
|
+
t.json("results");
|
|
4041
|
+
});
|
|
4042
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4043
|
+
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4044
|
+
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4045
|
+
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
4049
|
+
if (!tasksHasOpid) {
|
|
4050
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4051
|
+
t.text("opid");
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
4055
|
+
if (!tasksHasPausedAt) {
|
|
4056
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4057
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
if (needsHistory) {
|
|
4061
|
+
await db.schema.createTable(historyTable, (t) => {
|
|
4062
|
+
t.uuid("id").notNullable();
|
|
4063
|
+
t.timestamp("created_at").notNullable();
|
|
4064
|
+
t.timestamp("started_at");
|
|
4065
|
+
t.timestamp("completed_at");
|
|
4066
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4067
|
+
t.text("schedule");
|
|
4068
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4069
|
+
t.text("target").notNullable();
|
|
4070
|
+
t.text("task").notNullable();
|
|
4071
|
+
t.json("params");
|
|
4072
|
+
t.text("opid");
|
|
4073
|
+
t.text("progress");
|
|
4074
|
+
t.boolean("success");
|
|
4075
|
+
t.json("results");
|
|
4076
|
+
});
|
|
4077
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4078
|
+
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4079
|
+
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
|
|
4083
|
+
if (!historyHasOpid) {
|
|
4084
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4085
|
+
t.text("opid");
|
|
4086
|
+
});
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
async function enqueueTask(context, options) {
|
|
4090
|
+
const db = getDb(context);
|
|
4091
|
+
const queue = options.queue ?? "tasks";
|
|
4092
|
+
const { tasksTable } = queueToTableNames(queue);
|
|
4093
|
+
const id = randomUUID();
|
|
4094
|
+
await db(tasksTable).insert({
|
|
4095
|
+
id,
|
|
4096
|
+
target: options.target,
|
|
4097
|
+
task: options.task,
|
|
4098
|
+
params: toJsonColumn(options.params ?? null),
|
|
4099
|
+
opid: options.opid ?? null,
|
|
4100
|
+
priority: options.priority ?? 0,
|
|
4101
|
+
schedule: options.schedule ?? null
|
|
4102
|
+
});
|
|
4103
|
+
return id;
|
|
4104
|
+
}
|
|
4105
|
+
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
4106
|
+
const db = getDb(context);
|
|
4107
|
+
await db(tasksTable).where({ id: taskId }).update({
|
|
4108
|
+
progress: typeof progress === "string" ? progress : JSON.stringify(progress)
|
|
4109
|
+
});
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
// src/tasks/taskLogs.ts
|
|
4113
|
+
function getLogsState(context) {
|
|
4114
|
+
const holder = context;
|
|
4115
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4116
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4117
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4118
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4119
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4120
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4121
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4122
|
+
const errorDb = new FileDatabase({
|
|
4123
|
+
basePath,
|
|
4124
|
+
namespace,
|
|
4125
|
+
tableName: errorTableName,
|
|
4126
|
+
versioned: true,
|
|
4127
|
+
useMetadata: true,
|
|
4128
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4129
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4130
|
+
logger: holder.logger
|
|
4131
|
+
});
|
|
4132
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4133
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4134
|
+
if (!enabled) {
|
|
4135
|
+
const disabledState = {
|
|
4136
|
+
db: null,
|
|
4137
|
+
errorDb,
|
|
4138
|
+
queue: Promise.resolve(),
|
|
4139
|
+
initialized: true,
|
|
4140
|
+
errorInitialized: false
|
|
4141
|
+
};
|
|
4142
|
+
holder.__tasksLogsState = disabledState;
|
|
4143
|
+
return disabledState;
|
|
4144
|
+
}
|
|
4145
|
+
const db = new FileDatabase({
|
|
4146
|
+
basePath,
|
|
4147
|
+
namespace,
|
|
4148
|
+
tableName,
|
|
4149
|
+
versioned: true,
|
|
4150
|
+
useMetadata: true,
|
|
4151
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4152
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4153
|
+
logger: holder.logger
|
|
4154
|
+
});
|
|
4155
|
+
const state = {
|
|
4156
|
+
db,
|
|
4157
|
+
errorDb,
|
|
4158
|
+
queue: Promise.resolve(),
|
|
4159
|
+
initialized: false,
|
|
4160
|
+
errorInitialized: false
|
|
4161
|
+
};
|
|
4162
|
+
holder.__tasksLogsState = state;
|
|
4163
|
+
return state;
|
|
4164
|
+
}
|
|
4165
|
+
function isErrorPayload(payload) {
|
|
4166
|
+
if (!payload) return false;
|
|
4167
|
+
if (typeof payload === "object") {
|
|
4168
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4169
|
+
if (level === "error" || level === "fatal") return true;
|
|
4170
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4171
|
+
return false;
|
|
4172
|
+
}
|
|
4173
|
+
if (typeof payload === "string") {
|
|
4174
|
+
return /\berror\b/i.test(payload);
|
|
4175
|
+
}
|
|
4176
|
+
return false;
|
|
4177
|
+
}
|
|
4178
|
+
function buildLogRecord(task, payload) {
|
|
4179
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4180
|
+
return {
|
|
4181
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4182
|
+
opid: task.opid ?? null,
|
|
4183
|
+
taskId: task.id,
|
|
4184
|
+
taskName: task.task,
|
|
4185
|
+
target: task.target,
|
|
4186
|
+
source: typeof params.source === "string" ? params.source : null,
|
|
4187
|
+
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4188
|
+
payload
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function appendTaskIpcLog(context, task, payload) {
|
|
4192
|
+
const state = getLogsState(context);
|
|
4193
|
+
if (!state.db && !state.errorDb) return;
|
|
4194
|
+
const record = buildLogRecord(task, payload);
|
|
4195
|
+
state.queue = state.queue.then(async () => {
|
|
4196
|
+
if (state.db) {
|
|
4197
|
+
await state.db.write([record], { forceNewVersion: !state.initialized });
|
|
4198
|
+
state.initialized = true;
|
|
4199
|
+
}
|
|
4200
|
+
if (state.errorDb && isErrorPayload(payload)) {
|
|
4201
|
+
await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
|
|
4202
|
+
state.errorInitialized = true;
|
|
4203
|
+
}
|
|
4204
|
+
}).catch((error) => {
|
|
4205
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// src/tasks/time-matcher.ts
|
|
4210
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4211
|
+
function resolveAsterisks(field, range) {
|
|
4212
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4213
|
+
}
|
|
4214
|
+
function resolveRanges(field) {
|
|
4215
|
+
const regex = /(\d+)-(\d+)/;
|
|
4216
|
+
let current = field;
|
|
4217
|
+
while (true) {
|
|
4218
|
+
const match = regex.exec(current);
|
|
4219
|
+
if (!match) break;
|
|
4220
|
+
const raw = match[0];
|
|
4221
|
+
let first = Number(match[1]);
|
|
4222
|
+
let last = Number(match[2]);
|
|
4223
|
+
if (last < first) {
|
|
4224
|
+
[first, last] = [last, first];
|
|
4225
|
+
}
|
|
4226
|
+
const values = [];
|
|
4227
|
+
for (let i = first; i <= last; i += 1) {
|
|
4228
|
+
values.push(i);
|
|
4229
|
+
}
|
|
4230
|
+
current = current.replace(raw, values.join(","));
|
|
4231
|
+
}
|
|
4232
|
+
return current;
|
|
4233
|
+
}
|
|
4234
|
+
function resolveSteps(field) {
|
|
4235
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4236
|
+
if (!match) return field;
|
|
4237
|
+
const base = match[1];
|
|
4238
|
+
const step = Number(match[2]);
|
|
4239
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4240
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4241
|
+
}
|
|
4242
|
+
function convertPattern(pattern) {
|
|
4243
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4244
|
+
if (parts.length !== 6) {
|
|
4245
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4246
|
+
}
|
|
4247
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4248
|
+
}
|
|
4249
|
+
function fieldMatches(field, value) {
|
|
4250
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4251
|
+
return allowed.includes(value);
|
|
4252
|
+
}
|
|
4253
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4254
|
+
const parsed = convertPattern(pattern);
|
|
4255
|
+
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());
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// src/tasks/TaskMaster.ts
|
|
4259
|
+
var TaskMaster = class {
|
|
4260
|
+
context;
|
|
4261
|
+
task;
|
|
4262
|
+
constructor(context, task) {
|
|
4263
|
+
this.context = context;
|
|
4264
|
+
this.task = task;
|
|
4265
|
+
}
|
|
4266
|
+
cantRunReason() {
|
|
4267
|
+
return false;
|
|
4268
|
+
}
|
|
4269
|
+
requestStop(_allowanceMs) {
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
|
|
4273
|
+
// src/tasks/coreTasks/TaskPing.ts
|
|
4274
|
+
var TaskPing = class extends TaskMaster {
|
|
4275
|
+
async run() {
|
|
4276
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4277
|
+
return { success: true, results: "pong" };
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
|
|
4281
|
+
// src/tasks/coreTasks/TaskSampleProcess.ts
|
|
4282
|
+
var TaskSampleProcess = class extends TaskMaster {
|
|
4283
|
+
stopRequested = false;
|
|
4284
|
+
stopAllowanceMs = 0;
|
|
4285
|
+
stopDecisionLogged = false;
|
|
4286
|
+
requestStop(allowanceMs) {
|
|
4287
|
+
this.stopRequested = true;
|
|
4288
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
4289
|
+
this.context.logger.warn?.(
|
|
4290
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
async run(reportProgress) {
|
|
4294
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
4295
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
4296
|
+
const nameRaw = this.task?.params?.name;
|
|
4297
|
+
const total = Number(totalRaw);
|
|
4298
|
+
const delay = Number(delayRaw);
|
|
4299
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
4300
|
+
const errors = [];
|
|
4301
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4302
|
+
errors.push('param "total" must be a positive integer');
|
|
4303
|
+
}
|
|
4304
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4305
|
+
errors.push('param "delay" must be an integer >= 0');
|
|
4306
|
+
}
|
|
4307
|
+
if (errors.length > 0) {
|
|
4308
|
+
return {
|
|
4309
|
+
success: false,
|
|
4310
|
+
results: {
|
|
4311
|
+
error: `Validation failed: ${errors.join(", ")}`,
|
|
4312
|
+
received: { total: totalRaw, delay: delayRaw, name: nameRaw }
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
const startedAt = Date.now();
|
|
4317
|
+
for (let i = 1; i <= total; i += 1) {
|
|
4318
|
+
if (this.stopRequested) {
|
|
4319
|
+
const remainingMs = Math.max(0, (total - i + 1) * delay);
|
|
4320
|
+
if (remainingMs <= this.stopAllowanceMs) {
|
|
4321
|
+
if (!this.stopDecisionLogged) {
|
|
4322
|
+
this.stopDecisionLogged = true;
|
|
4323
|
+
this.context.logger.warn?.(
|
|
4324
|
+
`[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
|
|
4325
|
+
);
|
|
4326
|
+
}
|
|
4327
|
+
} else {
|
|
4328
|
+
this.context.logger.warn?.(
|
|
4329
|
+
`[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
|
|
4330
|
+
);
|
|
4331
|
+
return {
|
|
4332
|
+
success: false,
|
|
4333
|
+
results: {
|
|
4334
|
+
message: `Stopped before completion at iteration ${i}/${total}`,
|
|
4335
|
+
completed: i - 1,
|
|
4336
|
+
total,
|
|
4337
|
+
name,
|
|
4338
|
+
remainingMs,
|
|
4339
|
+
allowanceMs: this.stopAllowanceMs
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
const elapsed = Date.now() - startedAt;
|
|
4345
|
+
const remaining = Math.max(0, (total - i) * delay);
|
|
4346
|
+
const progress = {
|
|
4347
|
+
name,
|
|
4348
|
+
count: i,
|
|
4349
|
+
total,
|
|
4350
|
+
elapsedMs: elapsed,
|
|
4351
|
+
remainingMs: remaining,
|
|
4352
|
+
status: `running ${name}: ${i}/${total}`
|
|
4353
|
+
};
|
|
4354
|
+
this.context.logger.progress("running", {
|
|
4355
|
+
prefix: name,
|
|
4356
|
+
count: i,
|
|
4357
|
+
total
|
|
4358
|
+
});
|
|
4359
|
+
await reportProgress(progress);
|
|
4360
|
+
await sleepMs(delay);
|
|
4361
|
+
}
|
|
4362
|
+
return {
|
|
4363
|
+
success: true,
|
|
4364
|
+
results: {
|
|
4365
|
+
message: `Completed ${total} iterations`,
|
|
4366
|
+
total,
|
|
4367
|
+
delay,
|
|
4368
|
+
name
|
|
4369
|
+
}
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
};
|
|
4373
|
+
|
|
4374
|
+
// src/tasks/coreTasks/TaskShellCommand.ts
|
|
4375
|
+
import { spawn } from "child_process";
|
|
4376
|
+
function runShellCommand(command, cwd) {
|
|
4377
|
+
return new Promise((resolve2, reject) => {
|
|
4378
|
+
const child = spawn(command, {
|
|
4379
|
+
shell: true,
|
|
4380
|
+
cwd: cwd || process.cwd(),
|
|
4381
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4382
|
+
});
|
|
4383
|
+
let output = "";
|
|
4384
|
+
let stderr = "";
|
|
4385
|
+
child.stdout.on("data", (chunk) => {
|
|
4386
|
+
output += String(chunk);
|
|
4387
|
+
});
|
|
4388
|
+
child.stderr.on("data", (chunk) => {
|
|
4389
|
+
stderr += String(chunk);
|
|
4390
|
+
});
|
|
4391
|
+
child.on("error", (error) => {
|
|
4392
|
+
reject(error);
|
|
4393
|
+
});
|
|
4394
|
+
child.on("close", (exitCode, signal) => {
|
|
4395
|
+
resolve2({
|
|
4396
|
+
exitCode,
|
|
4397
|
+
output: output.trim(),
|
|
4398
|
+
stderr: stderr.trim(),
|
|
4399
|
+
signal
|
|
4400
|
+
});
|
|
4401
|
+
});
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
var TaskShellCommand = class extends TaskMaster {
|
|
4405
|
+
async run() {
|
|
4406
|
+
const params = this.task?.params;
|
|
4407
|
+
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
4408
|
+
const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
|
|
4409
|
+
const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
|
|
4410
|
+
const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
|
|
4411
|
+
if (!command) {
|
|
4412
|
+
return {
|
|
4413
|
+
success: false,
|
|
4414
|
+
results: {
|
|
4415
|
+
error: 'Validation failed: param "command" must be a non-empty string',
|
|
4416
|
+
received: this.task?.params ?? null
|
|
4417
|
+
}
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
try {
|
|
4421
|
+
const result = await runShellCommand(command, cwd);
|
|
4422
|
+
const success = result.exitCode === 0;
|
|
4423
|
+
this.context.logger.info?.(
|
|
4424
|
+
`[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
|
|
4425
|
+
);
|
|
4426
|
+
return {
|
|
4427
|
+
success,
|
|
4428
|
+
results: {
|
|
4429
|
+
command,
|
|
4430
|
+
cwd: cwd ?? process.cwd(),
|
|
4431
|
+
output: result.output,
|
|
4432
|
+
stderr: result.stderr,
|
|
4433
|
+
exitCode: result.exitCode,
|
|
4434
|
+
signal: result.signal
|
|
4435
|
+
}
|
|
4436
|
+
};
|
|
4437
|
+
} catch (error) {
|
|
4438
|
+
return {
|
|
4439
|
+
success: false,
|
|
4440
|
+
results: {
|
|
4441
|
+
command,
|
|
4442
|
+
cwd: cwd ?? process.cwd(),
|
|
4443
|
+
output: "",
|
|
4444
|
+
stderr: "",
|
|
4445
|
+
exitCode: null,
|
|
4446
|
+
error: error?.message ?? String(error)
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
};
|
|
4452
|
+
|
|
4453
|
+
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
4454
|
+
import os from "os";
|
|
4455
|
+
import fs4 from "fs/promises";
|
|
4456
|
+
function toGb(valueBytes) {
|
|
4457
|
+
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4458
|
+
}
|
|
4459
|
+
function toMb(valueBytes) {
|
|
4460
|
+
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4461
|
+
}
|
|
4462
|
+
async function getDiskStats() {
|
|
4463
|
+
const stats = await fs4.statfs("/");
|
|
4464
|
+
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4465
|
+
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4466
|
+
const used = total - free;
|
|
4467
|
+
return {
|
|
4468
|
+
total: toGb(total),
|
|
4469
|
+
used: toGb(used),
|
|
4470
|
+
free: toGb(free)
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
var TaskSystemInfo = class extends TaskMaster {
|
|
4474
|
+
async run() {
|
|
4475
|
+
try {
|
|
4476
|
+
const totalMemory = os.totalmem();
|
|
4477
|
+
const freeMemory = os.freemem();
|
|
4478
|
+
const usedMemory = totalMemory - freeMemory;
|
|
4479
|
+
const cpus = os.cpus();
|
|
4480
|
+
const cpuUtilization = cpus.map((cpu) => {
|
|
4481
|
+
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4482
|
+
const usage = (total - cpu.times.idle) / total * 100;
|
|
4483
|
+
return Number(usage.toFixed(2));
|
|
4484
|
+
});
|
|
4485
|
+
const processMemory = process.memoryUsage();
|
|
4486
|
+
const disk = await getDiskStats();
|
|
4487
|
+
const results = {
|
|
4488
|
+
memory: {
|
|
4489
|
+
total: toGb(totalMemory),
|
|
4490
|
+
used: toGb(usedMemory),
|
|
4491
|
+
free: toGb(freeMemory)
|
|
4492
|
+
},
|
|
4493
|
+
processMemory: {
|
|
4494
|
+
rss: toMb(processMemory.rss),
|
|
4495
|
+
heapTotal: toMb(processMemory.heapTotal),
|
|
4496
|
+
heapUsed: toMb(processMemory.heapUsed),
|
|
4497
|
+
external: toMb(processMemory.external)
|
|
4498
|
+
},
|
|
4499
|
+
disk,
|
|
4500
|
+
cpu: {
|
|
4501
|
+
cores: cpuUtilization.length,
|
|
4502
|
+
utilization: cpuUtilization
|
|
4503
|
+
},
|
|
4504
|
+
runtime: {
|
|
4505
|
+
platform: os.platform(),
|
|
4506
|
+
arch: os.arch(),
|
|
4507
|
+
uptimeSec: os.uptime(),
|
|
4508
|
+
hostname: os.hostname()
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
4512
|
+
return { success: true, results };
|
|
4513
|
+
} catch (error) {
|
|
4514
|
+
return {
|
|
4515
|
+
success: false,
|
|
4516
|
+
results: {
|
|
4517
|
+
error: "Can't collect system stats",
|
|
4518
|
+
message: error?.message ?? String(error)
|
|
4519
|
+
}
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
};
|
|
4524
|
+
|
|
4525
|
+
// src/tasks/coreTasks/TaskSumAB.ts
|
|
4526
|
+
var TaskSumAB = class extends TaskMaster {
|
|
4527
|
+
async run() {
|
|
4528
|
+
const a = this.task?.params?.a;
|
|
4529
|
+
const b = this.task?.params?.b;
|
|
4530
|
+
if (typeof a !== "number" || Number.isNaN(a)) {
|
|
4531
|
+
return {
|
|
4532
|
+
success: false,
|
|
4533
|
+
results: {
|
|
4534
|
+
error: 'Validation failed: param "a" must be a valid number',
|
|
4535
|
+
received: { a, b }
|
|
4536
|
+
}
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
if (typeof b !== "number" || Number.isNaN(b)) {
|
|
4540
|
+
return {
|
|
4541
|
+
success: false,
|
|
4542
|
+
results: {
|
|
4543
|
+
error: 'Validation failed: param "b" must be a valid number',
|
|
4544
|
+
received: { a, b }
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
const sum = a + b;
|
|
4549
|
+
this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
|
|
4550
|
+
return {
|
|
4551
|
+
success: true,
|
|
4552
|
+
results: { a, b, sum }
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
};
|
|
4556
|
+
|
|
4557
|
+
// src/tasks/coreTasks/TaskStopRunner.ts
|
|
4558
|
+
var TaskStopRunner = class extends TaskMaster {
|
|
4559
|
+
async run() {
|
|
4560
|
+
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4561
|
+
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
4562
|
+
return {
|
|
4563
|
+
success: true,
|
|
4564
|
+
results: {
|
|
4565
|
+
stopRunner: true,
|
|
4566
|
+
allowanceMs,
|
|
4567
|
+
message: "Runner stop requested"
|
|
4568
|
+
}
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
|
|
4573
|
+
// src/tasks/TasksRegistry.ts
|
|
4574
|
+
var TasksRegistry = class _TasksRegistry {
|
|
4575
|
+
map = {};
|
|
4576
|
+
constructor(initial) {
|
|
4577
|
+
if (initial) {
|
|
4578
|
+
this.addMany(initial);
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
static withCoreTasks() {
|
|
4582
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
4583
|
+
}
|
|
4584
|
+
add(taskName, taskClass) {
|
|
4585
|
+
this.map[taskName] = taskClass;
|
|
4586
|
+
return this;
|
|
4587
|
+
}
|
|
4588
|
+
addMany(entries) {
|
|
4589
|
+
for (const [name, klass] of Object.entries(entries)) {
|
|
4590
|
+
this.add(name, klass);
|
|
4591
|
+
}
|
|
4592
|
+
return this;
|
|
4593
|
+
}
|
|
4594
|
+
get(taskName) {
|
|
4595
|
+
return this.map[taskName];
|
|
4596
|
+
}
|
|
4597
|
+
listSupportedTasks() {
|
|
4598
|
+
return Object.keys(this.map).sort();
|
|
4599
|
+
}
|
|
4600
|
+
toObject() {
|
|
4601
|
+
return { ...this.map };
|
|
4602
|
+
}
|
|
4603
|
+
};
|
|
4604
|
+
|
|
4605
|
+
// src/tasks/taskScriptRunner.ts
|
|
4606
|
+
import { spawn as spawn2 } from "child_process";
|
|
4607
|
+
function toCliArgs(args = []) {
|
|
4608
|
+
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4609
|
+
}
|
|
4610
|
+
function formatChildLogPrefix(task) {
|
|
4611
|
+
return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4612
|
+
}
|
|
4613
|
+
async function runNodeTaskScript(context, options) {
|
|
4614
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
4615
|
+
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4616
|
+
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
4617
|
+
const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
|
|
4618
|
+
const child = spawn2(
|
|
4619
|
+
process.execPath,
|
|
4620
|
+
nodeArgs,
|
|
4621
|
+
{
|
|
4622
|
+
cwd: options.cwd || process.cwd(),
|
|
4623
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
4624
|
+
env: {
|
|
4625
|
+
...process.env,
|
|
4626
|
+
TASK_ID: options.task.id,
|
|
4627
|
+
TASK_NAME: options.task.task,
|
|
4628
|
+
TASK_OPID: options.task.opid || ""
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
);
|
|
4632
|
+
let stdout = "";
|
|
4633
|
+
let stderr = "";
|
|
4634
|
+
let workerResult = null;
|
|
4635
|
+
let hadErrorMessage = false;
|
|
4636
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
4637
|
+
const db = context.db;
|
|
4638
|
+
const tasksTable = context.params?.get?.("table") || "tasks";
|
|
4639
|
+
let progressWriteChain = Promise.resolve();
|
|
4640
|
+
let progressCallbackChain = Promise.resolve();
|
|
4641
|
+
const updateProgress = (text) => {
|
|
4642
|
+
if (!db || !text || !text.trim()) return;
|
|
4643
|
+
progressWriteChain = progressWriteChain.then(async () => {
|
|
4644
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
4645
|
+
}).catch((error) => {
|
|
4646
|
+
context.logger.warn?.(
|
|
4647
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4648
|
+
);
|
|
4649
|
+
});
|
|
4650
|
+
if (options.onProgress) {
|
|
4651
|
+
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
4652
|
+
await options.onProgress?.(text.slice(0, 4e3));
|
|
4653
|
+
}).catch((error) => {
|
|
4654
|
+
context.logger.warn?.(
|
|
4655
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4656
|
+
);
|
|
4657
|
+
});
|
|
4658
|
+
}
|
|
4659
|
+
};
|
|
4660
|
+
const payloadToProgressText = (payload) => {
|
|
4661
|
+
if (!payload) return "";
|
|
4662
|
+
if (typeof payload === "string") return payload;
|
|
4663
|
+
if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4664
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4665
|
+
return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
|
|
4666
|
+
}
|
|
4667
|
+
if (typeof payload.message === "string") return payload.message;
|
|
4668
|
+
if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4669
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4670
|
+
return `${pfx}${payload.count}/${payload.total}`;
|
|
4671
|
+
}
|
|
4672
|
+
return "";
|
|
4673
|
+
};
|
|
4674
|
+
child.stdout.on("data", (chunk) => {
|
|
4675
|
+
const text = String(chunk);
|
|
4676
|
+
stdout += text;
|
|
4677
|
+
if (text.trim()) {
|
|
4678
|
+
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4679
|
+
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4680
|
+
}
|
|
4681
|
+
});
|
|
4682
|
+
child.stderr.on("data", (chunk) => {
|
|
4683
|
+
const text = String(chunk);
|
|
4684
|
+
stderr += text;
|
|
4685
|
+
if (text.trim()) {
|
|
4686
|
+
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4687
|
+
}
|
|
4688
|
+
});
|
|
4689
|
+
child.on("message", (message) => {
|
|
4690
|
+
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4691
|
+
workerResult = message.__taskWorkerResult;
|
|
4692
|
+
return;
|
|
4693
|
+
}
|
|
4694
|
+
if (message && typeof message === "object") {
|
|
4695
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4696
|
+
if (level === "error" || level === "fatal") {
|
|
4697
|
+
hadErrorMessage = true;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
appendTaskIpcLog(context, options.task, message);
|
|
4701
|
+
const progressText = payloadToProgressText(message);
|
|
4702
|
+
if (progressText) {
|
|
4703
|
+
updateProgress(progressText);
|
|
4704
|
+
if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
|
|
4705
|
+
const countNum = Number(String(message.count).trim());
|
|
4706
|
+
const totalNum = Number(message.total);
|
|
4707
|
+
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
4708
|
+
context.logger.progress(message.message || "progress", {
|
|
4709
|
+
prefix: message.prefix || prefix,
|
|
4710
|
+
count: countNum,
|
|
4711
|
+
total: totalNum
|
|
4712
|
+
});
|
|
4713
|
+
} else {
|
|
4714
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4715
|
+
}
|
|
4716
|
+
} else {
|
|
4717
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
});
|
|
4721
|
+
return await new Promise((resolve2, reject) => {
|
|
4722
|
+
child.on("error", (error) => reject(error));
|
|
4723
|
+
child.on("close", (exitCode, signal) => {
|
|
4724
|
+
Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
|
|
4725
|
+
resolve2({
|
|
4726
|
+
exitCode,
|
|
4727
|
+
signal,
|
|
4728
|
+
stdout: stdout.trim(),
|
|
4729
|
+
stderr: stderr.trim(),
|
|
4730
|
+
workerResult,
|
|
4731
|
+
hadErrorMessage
|
|
4732
|
+
});
|
|
4733
|
+
});
|
|
4734
|
+
});
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
// src/tasks/index.ts
|
|
4739
|
+
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4740
|
+
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4741
|
+
function getDb2(context) {
|
|
4742
|
+
const db = context.db;
|
|
4743
|
+
if (!db) {
|
|
4744
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4745
|
+
}
|
|
4746
|
+
return db;
|
|
4747
|
+
}
|
|
4748
|
+
function normalizeRegistry(registry) {
|
|
4749
|
+
if (!registry) return defaultTasksRegistry;
|
|
4750
|
+
if (registry instanceof TasksRegistry) return registry;
|
|
4751
|
+
return new TasksRegistry().addMany(registry);
|
|
4752
|
+
}
|
|
4753
|
+
function normalizeAllowedTasks(value) {
|
|
4754
|
+
if (!value) return void 0;
|
|
4755
|
+
if (Array.isArray(value)) {
|
|
4756
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4757
|
+
return out2.length ? out2 : void 0;
|
|
4758
|
+
}
|
|
4759
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4760
|
+
return out.length ? out : void 0;
|
|
4761
|
+
}
|
|
4762
|
+
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
4763
|
+
return enqueueTask(context, {
|
|
4764
|
+
queue,
|
|
4765
|
+
target,
|
|
4766
|
+
task: "stopRunner",
|
|
4767
|
+
params: { allowanceMs },
|
|
4768
|
+
priority: 1e6
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4772
|
+
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4773
|
+
for (const [, taskInstance] of runningTaskInstances) {
|
|
4774
|
+
if (typeof taskInstance.requestStop === "function") {
|
|
4775
|
+
try {
|
|
4776
|
+
await taskInstance.requestStop(allowanceMs);
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
context.logger.warn?.("[tasks] task requestStop failed:", error);
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
context.emitter.emit("stop", allowanceMs);
|
|
4783
|
+
}
|
|
4784
|
+
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4785
|
+
const db = getDb2(context);
|
|
4786
|
+
const taskName = row.task;
|
|
4787
|
+
const TaskClass = registry.get(taskName);
|
|
4788
|
+
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4789
|
+
if (!TaskClass) {
|
|
4790
|
+
const err = { message: `Unknown task "${taskName}"` };
|
|
4791
|
+
await db(historyTable).insert({
|
|
4792
|
+
...rowForHistory,
|
|
4793
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4794
|
+
success: false,
|
|
4795
|
+
params: toJsonColumn(row.params),
|
|
4796
|
+
results: toJsonColumn(err)
|
|
4797
|
+
});
|
|
4798
|
+
if (row.schedule) {
|
|
4799
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4800
|
+
started_at: null,
|
|
4801
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4802
|
+
success: false,
|
|
4803
|
+
results: toJsonColumn(err),
|
|
4804
|
+
past_due: null,
|
|
4805
|
+
paused_at: db.fn.now(),
|
|
4806
|
+
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4807
|
+
});
|
|
4808
|
+
} else {
|
|
4809
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4810
|
+
}
|
|
4811
|
+
return { stopRunnerRequested: false, stopAllowanceMs: 0 };
|
|
4812
|
+
}
|
|
4813
|
+
let success = false;
|
|
4814
|
+
let results = null;
|
|
4815
|
+
let taskInstance = null;
|
|
4816
|
+
try {
|
|
4817
|
+
taskInstance = new TaskClass(context, row);
|
|
4818
|
+
runningTaskInstances.set(row.id, taskInstance);
|
|
4819
|
+
const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
|
|
4820
|
+
success = !!runResult?.success;
|
|
4821
|
+
results = runResult?.results ?? null;
|
|
4822
|
+
} catch (error) {
|
|
4823
|
+
success = false;
|
|
4824
|
+
results = {
|
|
4825
|
+
message: error?.message ?? String(error),
|
|
4826
|
+
name: error?.name ?? "Error",
|
|
4827
|
+
stack: error?.stack ?? null
|
|
4828
|
+
};
|
|
4829
|
+
} finally {
|
|
4830
|
+
runningTaskInstances.delete(row.id);
|
|
4831
|
+
}
|
|
4832
|
+
await db(historyTable).insert({
|
|
4833
|
+
...rowForHistory,
|
|
4834
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4835
|
+
success,
|
|
4836
|
+
params: toJsonColumn(row.params),
|
|
4837
|
+
results: toJsonColumn(results)
|
|
4838
|
+
});
|
|
4839
|
+
if (!success) {
|
|
4840
|
+
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4841
|
+
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
4842
|
+
const fallbackRecoverCommand = [
|
|
4843
|
+
"npx",
|
|
4844
|
+
"tsx",
|
|
4845
|
+
"examples/tasks/recover-task.ts",
|
|
4846
|
+
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
4847
|
+
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
4848
|
+
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
4849
|
+
].join(" ");
|
|
4850
|
+
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4851
|
+
appendTaskIpcLog(context, row, {
|
|
4852
|
+
level: "error",
|
|
4853
|
+
message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4854
|
+
details: results
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
if (row.schedule) {
|
|
4858
|
+
if (success) {
|
|
4859
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4860
|
+
started_at: null,
|
|
4861
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4862
|
+
success,
|
|
4863
|
+
results: toJsonColumn(results),
|
|
4864
|
+
progress: null,
|
|
4865
|
+
past_due: null
|
|
4866
|
+
});
|
|
4867
|
+
} else {
|
|
4868
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4869
|
+
started_at: null,
|
|
4870
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
4871
|
+
success,
|
|
4872
|
+
results: toJsonColumn(results),
|
|
4873
|
+
paused_at: db.fn.now(),
|
|
4874
|
+
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4875
|
+
past_due: null
|
|
4876
|
+
});
|
|
4877
|
+
}
|
|
4878
|
+
} else {
|
|
4879
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
4880
|
+
}
|
|
4881
|
+
const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
|
|
4882
|
+
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4883
|
+
return { stopRunnerRequested, stopAllowanceMs };
|
|
4884
|
+
}
|
|
4885
|
+
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
4886
|
+
const db = getDb2(context);
|
|
4887
|
+
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);
|
|
4888
|
+
if (taskNames && taskNames.length > 0) {
|
|
4889
|
+
query = query.whereIn("task", taskNames);
|
|
4890
|
+
}
|
|
4891
|
+
const candidates = await query;
|
|
4892
|
+
for (const row of candidates) {
|
|
4893
|
+
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4894
|
+
continue;
|
|
4895
|
+
}
|
|
4896
|
+
const TaskClass = registry.get(row.task);
|
|
4897
|
+
if (TaskClass) {
|
|
4898
|
+
const taskInstance = new TaskClass(context, row);
|
|
4899
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
4900
|
+
if (reason) {
|
|
4901
|
+
if (!row.past_due) {
|
|
4902
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
4903
|
+
past_due: db.fn.now(),
|
|
4904
|
+
progress: String(reason)
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
continue;
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
|
|
4911
|
+
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
4912
|
+
if (claimed) return claimed;
|
|
4913
|
+
}
|
|
4914
|
+
return null;
|
|
4915
|
+
}
|
|
4916
|
+
async function runTasksLoop(context, options) {
|
|
4917
|
+
const queue = options.queue ?? "tasks";
|
|
4918
|
+
const target = options.target;
|
|
4919
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
4920
|
+
const maxParallel = options.maxParallel ?? 1;
|
|
4921
|
+
const scanLimit = options.scanLimit ?? 100;
|
|
4922
|
+
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4923
|
+
const registry = normalizeRegistry(options.registry);
|
|
4924
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4925
|
+
if (!target) throw new Error("runTasksLoop: target is required");
|
|
4926
|
+
const runningPromises = /* @__PURE__ */ new Set();
|
|
4927
|
+
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
4928
|
+
let runningStopControlPromise = null;
|
|
4929
|
+
let stopRequested = false;
|
|
4930
|
+
let stopAllowanceMs = 5e3;
|
|
4931
|
+
context.__tasksRunnerStop = false;
|
|
4932
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
4933
|
+
if (!runningStopControlPromise) {
|
|
4934
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
4935
|
+
context,
|
|
4936
|
+
tasksTable,
|
|
4937
|
+
target,
|
|
4938
|
+
registry,
|
|
4939
|
+
10,
|
|
4940
|
+
["stopRunner", "stop"]
|
|
4941
|
+
);
|
|
4942
|
+
if (claimedStopTask) {
|
|
4943
|
+
runningStopControlPromise = executeClaimedTask(
|
|
4944
|
+
context,
|
|
4945
|
+
tasksTable,
|
|
4946
|
+
historyTable,
|
|
4947
|
+
claimedStopTask,
|
|
4948
|
+
registry,
|
|
4949
|
+
runningTaskInstances
|
|
4950
|
+
).then(async (outcome) => {
|
|
4951
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4952
|
+
stopRequested = true;
|
|
4953
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4954
|
+
context.__tasksRunnerStop = true;
|
|
4955
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4956
|
+
}
|
|
4957
|
+
}).finally(() => {
|
|
4958
|
+
runningStopControlPromise = null;
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
while (runningPromises.size < maxParallel) {
|
|
4963
|
+
const claimed = await claimNextRunnableTask(
|
|
4964
|
+
context,
|
|
4965
|
+
tasksTable,
|
|
4966
|
+
target,
|
|
4967
|
+
registry,
|
|
4968
|
+
scanLimit,
|
|
4969
|
+
allowedTasks
|
|
4970
|
+
);
|
|
4971
|
+
if (!claimed) break;
|
|
4972
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4973
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4974
|
+
stopRequested = true;
|
|
4975
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4976
|
+
context.__tasksRunnerStop = true;
|
|
4977
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4978
|
+
}
|
|
4979
|
+
}).finally(() => {
|
|
4980
|
+
runningPromises.delete(p);
|
|
4981
|
+
});
|
|
4982
|
+
runningPromises.add(p);
|
|
4983
|
+
}
|
|
4984
|
+
await sleepMs(pollMs);
|
|
4985
|
+
}
|
|
4986
|
+
if (context.isStop() && !stopRequested) {
|
|
4987
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
4988
|
+
}
|
|
4989
|
+
if (runningPromises.size > 0) {
|
|
4990
|
+
if (stopRequested) {
|
|
4991
|
+
await Promise.race([
|
|
4992
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
4993
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
4994
|
+
context.logger.warn?.(
|
|
4995
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
4996
|
+
);
|
|
4997
|
+
})
|
|
4998
|
+
]);
|
|
4999
|
+
} else {
|
|
5000
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5005
|
+
const db = getDb2(context);
|
|
5006
|
+
const queue = options.queue ?? "tasks";
|
|
5007
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5008
|
+
const pollMs = options.pollMs ?? 500;
|
|
5009
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5010
|
+
const deadline = Date.now() + timeoutMs;
|
|
5011
|
+
while (Date.now() <= deadline) {
|
|
5012
|
+
const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5013
|
+
if (done) return done;
|
|
5014
|
+
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5015
|
+
if (!pending) {
|
|
5016
|
+
const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5017
|
+
return maybeDone ?? null;
|
|
5018
|
+
}
|
|
5019
|
+
await sleepMs(pollMs);
|
|
5020
|
+
}
|
|
5021
|
+
return null;
|
|
5022
|
+
}
|
|
5023
|
+
var TasksManager = class _TasksManager {
|
|
5024
|
+
context;
|
|
5025
|
+
queue;
|
|
5026
|
+
target;
|
|
5027
|
+
recreateTaskTables;
|
|
5028
|
+
pollMs;
|
|
5029
|
+
maxParallel;
|
|
5030
|
+
scanLimit;
|
|
5031
|
+
allowedTasks;
|
|
5032
|
+
registry;
|
|
5033
|
+
constructor(context, options = {}) {
|
|
5034
|
+
this.context = context;
|
|
5035
|
+
this.queue = options.queue ?? "tasks";
|
|
5036
|
+
this.target = options.target ?? "localRunner";
|
|
5037
|
+
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5038
|
+
this.pollMs = options.pollMs ?? 1e3;
|
|
5039
|
+
this.maxParallel = options.maxParallel ?? 1;
|
|
5040
|
+
this.scanLimit = options.scanLimit ?? 100;
|
|
5041
|
+
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5042
|
+
this.registry = normalizeRegistry(options.registry);
|
|
5043
|
+
}
|
|
5044
|
+
static init(context, options = {}) {
|
|
5045
|
+
const defs = {
|
|
5046
|
+
table: "string default tasks",
|
|
5047
|
+
target: "string default localRunner",
|
|
5048
|
+
recreateTaskTables: "boolean default false",
|
|
5049
|
+
pollMs: "number default 1000",
|
|
5050
|
+
maxParallel: "number default 1",
|
|
5051
|
+
scanLimit: "number default 100",
|
|
5052
|
+
allowedTasks: "string"
|
|
5053
|
+
};
|
|
5054
|
+
const discovered = context.params.getAllForModule(defs);
|
|
5055
|
+
const resolved = {
|
|
5056
|
+
queue: discovered.table,
|
|
5057
|
+
target: discovered.target,
|
|
5058
|
+
recreateTaskTables: discovered.recreateTaskTables,
|
|
5059
|
+
pollMs: discovered.pollMs,
|
|
5060
|
+
maxParallel: discovered.maxParallel,
|
|
5061
|
+
scanLimit: discovered.scanLimit,
|
|
5062
|
+
allowedTasks: discovered.allowedTasks,
|
|
5063
|
+
...options
|
|
5064
|
+
};
|
|
5065
|
+
return new _TasksManager(context, resolved);
|
|
5066
|
+
}
|
|
5067
|
+
async ensureTaskTables(options = {}) {
|
|
5068
|
+
await ensureTaskTables(this.context, {
|
|
5069
|
+
queue: this.queue,
|
|
5070
|
+
recreate: options.recreate ?? this.recreateTaskTables
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
async runTasksLoop(options = {}) {
|
|
5074
|
+
await runTasksLoop(this.context, {
|
|
5075
|
+
queue: options.queue ?? this.queue,
|
|
5076
|
+
target: options.target ?? this.target,
|
|
5077
|
+
pollMs: options.pollMs ?? this.pollMs,
|
|
5078
|
+
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5079
|
+
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5080
|
+
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5081
|
+
registry: options.registry ?? this.registry
|
|
5082
|
+
});
|
|
5083
|
+
}
|
|
5084
|
+
};
|
|
3777
5085
|
export {
|
|
3778
5086
|
Args,
|
|
3779
5087
|
Box5 as Box,
|
|
@@ -3796,8 +5104,18 @@ export {
|
|
|
3796
5104
|
ScreenFooter,
|
|
3797
5105
|
ScreenRow,
|
|
3798
5106
|
ScreenTitle,
|
|
5107
|
+
TaskMaster,
|
|
5108
|
+
TaskPing,
|
|
5109
|
+
TaskSampleProcess,
|
|
5110
|
+
TaskShellCommand,
|
|
5111
|
+
TaskStopRunner,
|
|
5112
|
+
TaskSumAB,
|
|
5113
|
+
TaskSystemInfo,
|
|
5114
|
+
TasksManager,
|
|
5115
|
+
TasksRegistry,
|
|
3799
5116
|
Text5 as Text,
|
|
3800
5117
|
TextBlock,
|
|
5118
|
+
appendTaskIpcLog,
|
|
3801
5119
|
buildBreadcrumb,
|
|
3802
5120
|
buildDetailBreadcrumb,
|
|
3803
5121
|
buildFooter,
|
|
@@ -3805,8 +5123,11 @@ export {
|
|
|
3805
5123
|
dbFindAndConnect,
|
|
3806
5124
|
dbInit,
|
|
3807
5125
|
defaultFileSynopsisFunction,
|
|
5126
|
+
defaultTasksRegistry,
|
|
3808
5127
|
defaultVersionSynopsisFunction,
|
|
3809
|
-
|
|
5128
|
+
enqueueStopTask,
|
|
5129
|
+
enqueueTask,
|
|
5130
|
+
ensureTaskTables,
|
|
3810
5131
|
getArgsInstance,
|
|
3811
5132
|
createElement2 as h,
|
|
3812
5133
|
joiEdateType,
|
|
@@ -3815,6 +5136,9 @@ export {
|
|
|
3815
5136
|
listTables,
|
|
3816
5137
|
load,
|
|
3817
5138
|
organizeFooterMessages,
|
|
5139
|
+
queueToTableNames,
|
|
5140
|
+
runNodeTaskScript,
|
|
5141
|
+
runTasksLoop,
|
|
3818
5142
|
setupContext,
|
|
3819
5143
|
showListScreen,
|
|
3820
5144
|
showMenuScreen,
|
|
@@ -3822,11 +5146,13 @@ export {
|
|
|
3822
5146
|
showMultiColumnListWithPreviewScreen,
|
|
3823
5147
|
showScreen,
|
|
3824
5148
|
showWordGridScreen,
|
|
5149
|
+
updateTaskProgress,
|
|
3825
5150
|
useCallback,
|
|
3826
5151
|
useEffect3 as useEffect,
|
|
3827
5152
|
useInput2 as useInput,
|
|
3828
5153
|
useMemo,
|
|
3829
5154
|
useRef3 as useRef,
|
|
3830
|
-
useState3 as useState
|
|
5155
|
+
useState3 as useState,
|
|
5156
|
+
waitForTaskResult
|
|
3831
5157
|
};
|
|
3832
5158
|
//# sourceMappingURL=index.js.map
|