@nmakarov/cli-toolkit 0.43.0 → 0.46.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/cli-runner.cjs +192 -9
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +192 -9
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +163 -4
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +163 -4
- package/dist/db.js.map +1 -1
- package/dist/filedatabase.cjs +26 -20
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +26 -20
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +1 -1
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +1 -1
- package/dist/http-client.js.map +1 -1
- package/dist/index.cjs +232 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +232 -31
- package/dist/index.js.map +1 -1
- package/dist/tasks.cjs +43 -7
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +43 -7
- package/dist/tasks.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2283,23 +2283,18 @@ function defaultFileSynopsisFunction(fileEntry, data) {
|
|
|
2283
2283
|
const timestamps = [];
|
|
2284
2284
|
const statusCounts = {};
|
|
2285
2285
|
for (const item of data) {
|
|
2286
|
-
|
|
2287
|
-
let status = null;
|
|
2286
|
+
if (!item || typeof item !== "object") continue;
|
|
2288
2287
|
for (const [key, value] of Object.entries(item)) {
|
|
2289
2288
|
const k = key.toLowerCase();
|
|
2290
|
-
if (k
|
|
2291
|
-
ts = new Date(value).getTime();
|
|
2289
|
+
if (k.endsWith("modificationtimestamp") && value != null && value !== "") {
|
|
2290
|
+
const ts = new Date(value).getTime();
|
|
2291
|
+
if (!Number.isNaN(ts)) timestamps.push(ts);
|
|
2292
2292
|
}
|
|
2293
|
-
if (k === "standardstatus") {
|
|
2294
|
-
status = value;
|
|
2293
|
+
if (k === "standardstatus" && value != null && value !== "") {
|
|
2294
|
+
const status = String(value);
|
|
2295
|
+
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
2295
2296
|
}
|
|
2296
2297
|
}
|
|
2297
|
-
if (ts && !isNaN(ts)) {
|
|
2298
|
-
timestamps.push(ts);
|
|
2299
|
-
}
|
|
2300
|
-
if (status !== null && status !== void 0) {
|
|
2301
|
-
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
2302
|
-
}
|
|
2303
2298
|
}
|
|
2304
2299
|
const result = { ...fileEntry };
|
|
2305
2300
|
if (timestamps.length) {
|
|
@@ -2318,27 +2313,38 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2318
2313
|
const timestamps = [];
|
|
2319
2314
|
const statusCounts = {};
|
|
2320
2315
|
for (const file of metadata.files) {
|
|
2321
|
-
if (file
|
|
2316
|
+
if (file?.minModificationTimestamp) {
|
|
2322
2317
|
const minTs = new Date(file.minModificationTimestamp).getTime();
|
|
2323
|
-
if (!isNaN(minTs)) timestamps.push(minTs);
|
|
2318
|
+
if (!Number.isNaN(minTs)) timestamps.push(minTs);
|
|
2324
2319
|
}
|
|
2325
|
-
if (file
|
|
2320
|
+
if (file?.maxModificationTimestamp) {
|
|
2326
2321
|
const maxTs = new Date(file.maxModificationTimestamp).getTime();
|
|
2327
|
-
if (!isNaN(maxTs)) timestamps.push(maxTs);
|
|
2322
|
+
if (!Number.isNaN(maxTs)) timestamps.push(maxTs);
|
|
2328
2323
|
}
|
|
2329
|
-
if (file
|
|
2324
|
+
if (file?.StandardStatuses && typeof file.StandardStatuses === "object") {
|
|
2330
2325
|
for (const [status, count] of Object.entries(file.StandardStatuses)) {
|
|
2331
|
-
statusCounts[status] = (statusCounts[status] || 0) + count;
|
|
2326
|
+
statusCounts[status] = (statusCounts[status] || 0) + Number(count || 0);
|
|
2332
2327
|
}
|
|
2333
2328
|
}
|
|
2334
2329
|
}
|
|
2335
2330
|
const result = { ...metadata };
|
|
2331
|
+
const synopsis = {
|
|
2332
|
+
...metadata.synopsis && typeof metadata.synopsis === "object" ? metadata.synopsis : {}
|
|
2333
|
+
};
|
|
2336
2334
|
if (timestamps.length) {
|
|
2337
|
-
|
|
2338
|
-
|
|
2335
|
+
const minIso = new Date(Math.min(...timestamps)).toISOString();
|
|
2336
|
+
const maxIso = new Date(Math.max(...timestamps)).toISOString();
|
|
2337
|
+
result.minModificationTimestamp = minIso;
|
|
2338
|
+
result.maxModificationTimestamp = maxIso;
|
|
2339
|
+
synopsis.minModificationTimestamp = minIso;
|
|
2340
|
+
synopsis.maxModificationTimestamp = maxIso;
|
|
2339
2341
|
}
|
|
2340
2342
|
if (Object.keys(statusCounts).length > 0) {
|
|
2341
2343
|
result.StandardStatuses = statusCounts;
|
|
2344
|
+
synopsis.StandardStatuses = statusCounts;
|
|
2345
|
+
}
|
|
2346
|
+
if (Object.keys(synopsis).length) {
|
|
2347
|
+
result.synopsis = synopsis;
|
|
2342
2348
|
}
|
|
2343
2349
|
return result;
|
|
2344
2350
|
}
|
|
@@ -3472,10 +3478,35 @@ async function ensureSchemaEverywhere(dbs, spec, options = {}) {
|
|
|
3472
3478
|
// src/db/index.js
|
|
3473
3479
|
var KNEX_DEFAULTS = {
|
|
3474
3480
|
testConnection: true,
|
|
3475
|
-
|
|
3481
|
+
// min: 0 avoids holding idle sockets that go stale during long-running CLIs
|
|
3482
|
+
pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
|
|
3476
3483
|
acquireConnectionTimeout: 1e4,
|
|
3477
3484
|
ssl: { rejectUnauthorized: false }
|
|
3478
3485
|
};
|
|
3486
|
+
var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3487
|
+
"ECONNRESET",
|
|
3488
|
+
"ECONNREFUSED",
|
|
3489
|
+
"EPIPE",
|
|
3490
|
+
"ETIMEDOUT",
|
|
3491
|
+
"ENOTFOUND",
|
|
3492
|
+
"EHOSTUNREACH",
|
|
3493
|
+
"ENETUNREACH",
|
|
3494
|
+
"ECONNABORTED",
|
|
3495
|
+
"CONNECTION_ENDED",
|
|
3496
|
+
"CONNECTION_CLOSED",
|
|
3497
|
+
// PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
|
|
3498
|
+
"08000",
|
|
3499
|
+
"08001",
|
|
3500
|
+
"08003",
|
|
3501
|
+
"08004",
|
|
3502
|
+
"08006",
|
|
3503
|
+
"08007",
|
|
3504
|
+
"08P01",
|
|
3505
|
+
"57P01",
|
|
3506
|
+
"57P02",
|
|
3507
|
+
"57P03"
|
|
3508
|
+
]);
|
|
3509
|
+
var CONNECTION_ERROR_MESSAGE_RE = /connection (terminated|ended|closed|destroyed|reset|refused|not open)|Connection terminated unexpectedly|Client has encountered a connection error|server closed the connection|Cannot use a pool after calling end|This socket has been ended|connect ECONNRESET|Timeout acquiring a connection/i;
|
|
3479
3510
|
var Db = class _Db {
|
|
3480
3511
|
static async init(context, options = {}) {
|
|
3481
3512
|
const buildConfig = async () => {
|
|
@@ -3723,10 +3754,11 @@ var Db = class _Db {
|
|
|
3723
3754
|
this.knexInstance = null;
|
|
3724
3755
|
this.isConnected = false;
|
|
3725
3756
|
this.queriesLog = [];
|
|
3757
|
+
this._reconnectPromise = null;
|
|
3726
3758
|
this.config = {
|
|
3727
3759
|
testConnection: true,
|
|
3728
3760
|
profile: false,
|
|
3729
|
-
pool: { min:
|
|
3761
|
+
pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
|
|
3730
3762
|
acquireConnectionTimeout: 1e4,
|
|
3731
3763
|
ssl: { rejectUnauthorized: false },
|
|
3732
3764
|
logger: console,
|
|
@@ -3744,7 +3776,7 @@ var Db = class _Db {
|
|
|
3744
3776
|
if (!inst.knexInstance) {
|
|
3745
3777
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3746
3778
|
}
|
|
3747
|
-
return inst.knexInstance(...argumentsList);
|
|
3779
|
+
return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
|
|
3748
3780
|
},
|
|
3749
3781
|
get: (target, prop) => {
|
|
3750
3782
|
if (prop === "_instance") {
|
|
@@ -3754,8 +3786,12 @@ var Db = class _Db {
|
|
|
3754
3786
|
const ownMethods = [
|
|
3755
3787
|
"connect",
|
|
3756
3788
|
"disconnect",
|
|
3789
|
+
"reconnect",
|
|
3757
3790
|
"testConnection",
|
|
3758
3791
|
"tableExists",
|
|
3792
|
+
"raw",
|
|
3793
|
+
"withConnectionRetry",
|
|
3794
|
+
"isConnectionError",
|
|
3759
3795
|
"getQueryLog",
|
|
3760
3796
|
"getKnex",
|
|
3761
3797
|
"isConnectedToDb",
|
|
@@ -3775,6 +3811,9 @@ var Db = class _Db {
|
|
|
3775
3811
|
if (inst.knexInstance) {
|
|
3776
3812
|
const knexProp = inst.knexInstance[prop];
|
|
3777
3813
|
if (typeof knexProp === "function") {
|
|
3814
|
+
if (prop === "raw") {
|
|
3815
|
+
return (...args) => inst.raw(...args);
|
|
3816
|
+
}
|
|
3778
3817
|
return knexProp.bind(inst.knexInstance);
|
|
3779
3818
|
}
|
|
3780
3819
|
return knexProp;
|
|
@@ -3854,6 +3893,130 @@ var Db = class _Db {
|
|
|
3854
3893
|
throw error;
|
|
3855
3894
|
}
|
|
3856
3895
|
}
|
|
3896
|
+
/**
|
|
3897
|
+
* True when the error indicates a dead socket / pool that a fresh connect may fix.
|
|
3898
|
+
* Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
|
|
3899
|
+
*/
|
|
3900
|
+
isConnectionError(error) {
|
|
3901
|
+
if (!error) {
|
|
3902
|
+
return false;
|
|
3903
|
+
}
|
|
3904
|
+
if (error instanceof AggregateError && Array.isArray(error.errors)) {
|
|
3905
|
+
return error.errors.some((e) => this.isConnectionError(e));
|
|
3906
|
+
}
|
|
3907
|
+
const code = error.code ?? error.errno;
|
|
3908
|
+
if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
|
|
3909
|
+
return true;
|
|
3910
|
+
}
|
|
3911
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3912
|
+
return CONNECTION_ERROR_MESSAGE_RE.test(msg);
|
|
3913
|
+
}
|
|
3914
|
+
/**
|
|
3915
|
+
* Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
|
|
3916
|
+
*/
|
|
3917
|
+
async reconnect() {
|
|
3918
|
+
if (this._reconnectPromise) {
|
|
3919
|
+
await this._reconnectPromise;
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3922
|
+
this._reconnectPromise = (async () => {
|
|
3923
|
+
const old = this.knexInstance;
|
|
3924
|
+
this.isConnected = false;
|
|
3925
|
+
this.knexInstance = null;
|
|
3926
|
+
this.queriesLog = [];
|
|
3927
|
+
if (old) {
|
|
3928
|
+
try {
|
|
3929
|
+
await old.destroy();
|
|
3930
|
+
} catch (error) {
|
|
3931
|
+
this.logger.debug?.(
|
|
3932
|
+
`[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
await this.connect();
|
|
3937
|
+
})();
|
|
3938
|
+
try {
|
|
3939
|
+
await this._reconnectPromise;
|
|
3940
|
+
} finally {
|
|
3941
|
+
this._reconnectPromise = null;
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
async reconnectAfterConnectionError(error) {
|
|
3945
|
+
this.logger.warn?.(
|
|
3946
|
+
`[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
|
|
3947
|
+
);
|
|
3948
|
+
await this.reconnect();
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
|
|
3952
|
+
* `fn` should look up `this.knexInstance` each call so the retry uses the new client.
|
|
3953
|
+
*/
|
|
3954
|
+
async withConnectionRetry(fn, { retries = 1 } = {}) {
|
|
3955
|
+
let lastError;
|
|
3956
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
3957
|
+
try {
|
|
3958
|
+
return await fn();
|
|
3959
|
+
} catch (error) {
|
|
3960
|
+
lastError = error;
|
|
3961
|
+
if (!this.isConnectionError(error) || attempt >= retries) {
|
|
3962
|
+
throw error;
|
|
3963
|
+
}
|
|
3964
|
+
await this.reconnectAfterConnectionError(error);
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
throw lastError;
|
|
3968
|
+
}
|
|
3969
|
+
/**
|
|
3970
|
+
* Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
|
|
3971
|
+
* when the first attempt dies with a connection error.
|
|
3972
|
+
*/
|
|
3973
|
+
wrapQueryBuilder(builder) {
|
|
3974
|
+
if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
|
|
3975
|
+
return builder;
|
|
3976
|
+
}
|
|
3977
|
+
builder.__dbReconnectWrapped = true;
|
|
3978
|
+
const inst = this;
|
|
3979
|
+
const protoThen = Object.getPrototypeOf(builder)?.then;
|
|
3980
|
+
if (typeof protoThen !== "function") {
|
|
3981
|
+
return builder;
|
|
3982
|
+
}
|
|
3983
|
+
builder.then = function(onFulfilled, onRejected) {
|
|
3984
|
+
const run2 = async () => {
|
|
3985
|
+
try {
|
|
3986
|
+
return await protoThen.call(builder);
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
if (!inst.isConnectionError(error)) {
|
|
3989
|
+
throw error;
|
|
3990
|
+
}
|
|
3991
|
+
await inst.reconnectAfterConnectionError(error);
|
|
3992
|
+
if (typeof builder.clone === "function") {
|
|
3993
|
+
const retry = builder.clone();
|
|
3994
|
+
retry.client = inst.knexInstance.client;
|
|
3995
|
+
return await protoThen.call(retry);
|
|
3996
|
+
}
|
|
3997
|
+
if (typeof builder.toSQL === "function" && inst.knexInstance) {
|
|
3998
|
+
const sql = builder.toSQL();
|
|
3999
|
+
const statements = Array.isArray(sql) ? sql : [sql];
|
|
4000
|
+
let last;
|
|
4001
|
+
for (const stmt of statements) {
|
|
4002
|
+
last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
|
|
4003
|
+
}
|
|
4004
|
+
return last;
|
|
4005
|
+
}
|
|
4006
|
+
throw error;
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
return run2().then(onFulfilled, onRejected);
|
|
4010
|
+
};
|
|
4011
|
+
return builder;
|
|
4012
|
+
}
|
|
4013
|
+
/** knex.raw with auto-reconnect on dead connections. */
|
|
4014
|
+
raw(...args) {
|
|
4015
|
+
if (!this.knexInstance) {
|
|
4016
|
+
throw new Error("Db: Not connected. Call connect() first.");
|
|
4017
|
+
}
|
|
4018
|
+
return this.wrapQueryBuilder(this.knexInstance.raw(...args));
|
|
4019
|
+
}
|
|
3857
4020
|
getErrorMessage(error) {
|
|
3858
4021
|
if (error instanceof AggregateError) {
|
|
3859
4022
|
const errors = error.errors || [];
|
|
@@ -4024,7 +4187,9 @@ var Db = class _Db {
|
|
|
4024
4187
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
4025
4188
|
}
|
|
4026
4189
|
try {
|
|
4027
|
-
return await this.
|
|
4190
|
+
return await this.withConnectionRetry(
|
|
4191
|
+
() => this.knexInstance.schema.hasTable(tableName)
|
|
4192
|
+
);
|
|
4028
4193
|
} catch (error) {
|
|
4029
4194
|
this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
|
|
4030
4195
|
throw error;
|
|
@@ -6321,22 +6486,46 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
6321
6486
|
}
|
|
6322
6487
|
}
|
|
6323
6488
|
const { actions } = await ensureSchema(db, spec, { dryRun, logger: context.logger });
|
|
6489
|
+
const legacyDrops = await dropLegacyTaskNameColumn(db, [tasksTable, historyTable], {
|
|
6490
|
+
dryRun,
|
|
6491
|
+
log,
|
|
6492
|
+
label
|
|
6493
|
+
});
|
|
6494
|
+
const allActions = [...actions, ...legacyDrops];
|
|
6324
6495
|
if (dryRun) {
|
|
6325
|
-
if (
|
|
6496
|
+
if (allActions.length === 0) {
|
|
6326
6497
|
log.info?.(`[tasks-schema] dryRun \u2014 ${label}: queue "${queueName}" already up to date; no DDL`);
|
|
6327
6498
|
} else {
|
|
6328
6499
|
log.info?.(
|
|
6329
|
-
`[tasks-schema] dryRun \u2014 ${label}: would run ${
|
|
6500
|
+
`[tasks-schema] dryRun \u2014 ${label}: would run ${allActions.length} statement(s) for queue "${queueName}":`
|
|
6330
6501
|
);
|
|
6331
|
-
for (const s of
|
|
6502
|
+
for (const s of allActions) log.info?.(` - ${s}`);
|
|
6332
6503
|
}
|
|
6333
|
-
} else if (
|
|
6504
|
+
} else if (allActions.length > 0) {
|
|
6334
6505
|
log.info?.(
|
|
6335
|
-
`[tasks-schema] ${label}: applied ${
|
|
6506
|
+
`[tasks-schema] ${label}: applied ${allActions.length} DDL statement(s) for queue "${queueName}"`
|
|
6336
6507
|
);
|
|
6337
6508
|
}
|
|
6338
6509
|
}
|
|
6339
6510
|
}
|
|
6511
|
+
async function dropLegacyTaskNameColumn(db, tableNames, { dryRun, log, label }) {
|
|
6512
|
+
const actions = [];
|
|
6513
|
+
for (const table of tableNames) {
|
|
6514
|
+
if (!await db.tableExists(table).catch(() => false)) continue;
|
|
6515
|
+
const hasTask = await db.schema.hasColumn(table, "task");
|
|
6516
|
+
const hasName = await db.schema.hasColumn(table, "name");
|
|
6517
|
+
if (!hasTask || !hasName) continue;
|
|
6518
|
+
const sql = `ALTER TABLE "${table}" DROP COLUMN IF EXISTS "task"`;
|
|
6519
|
+
actions.push(sql);
|
|
6520
|
+
if (dryRun) {
|
|
6521
|
+
log?.info?.(`[tasks-schema] dryRun \u2014 ${label}: ${sql}`);
|
|
6522
|
+
} else {
|
|
6523
|
+
await db.raw(sql);
|
|
6524
|
+
log?.info?.(`[tasks-schema] ${label}: dropped legacy column ${table}.task`);
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
return actions;
|
|
6528
|
+
}
|
|
6340
6529
|
async function enqueueTask(context, options) {
|
|
6341
6530
|
const db = getDb(context);
|
|
6342
6531
|
const queueName = options.queueName ?? "tasks";
|
|
@@ -6353,7 +6542,7 @@ async function enqueueTask(context, options) {
|
|
|
6353
6542
|
} else if (schedule) {
|
|
6354
6543
|
nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
|
|
6355
6544
|
}
|
|
6356
|
-
|
|
6545
|
+
const row = {
|
|
6357
6546
|
id,
|
|
6358
6547
|
name,
|
|
6359
6548
|
params: toJsonColumn(options.params ?? null),
|
|
@@ -6367,9 +6556,21 @@ async function enqueueTask(context, options) {
|
|
|
6367
6556
|
server_name: options.serverName ?? null,
|
|
6368
6557
|
status: "idle",
|
|
6369
6558
|
status_changed_at: db.fn.now()
|
|
6370
|
-
}
|
|
6559
|
+
};
|
|
6560
|
+
if (await tableHasLegacyTaskColumn(db, tasksTable)) {
|
|
6561
|
+
row.task = name;
|
|
6562
|
+
}
|
|
6563
|
+
await db(tasksTable).insert(row);
|
|
6371
6564
|
return id;
|
|
6372
6565
|
}
|
|
6566
|
+
var legacyTaskColumnCache = /* @__PURE__ */ new Map();
|
|
6567
|
+
async function tableHasLegacyTaskColumn(db, tableName) {
|
|
6568
|
+
const key = `${db?.config?.name ?? "db"}:${tableName}`;
|
|
6569
|
+
if (!legacyTaskColumnCache.has(key)) {
|
|
6570
|
+
legacyTaskColumnCache.set(key, await db.schema.hasColumn(tableName, "task"));
|
|
6571
|
+
}
|
|
6572
|
+
return legacyTaskColumnCache.get(key);
|
|
6573
|
+
}
|
|
6373
6574
|
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
6374
6575
|
const db = getDb(context);
|
|
6375
6576
|
await db(tasksTable).where({ id: taskId }).update({
|