@nmakarov/cli-toolkit 0.44.0 → 0.47.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.
@@ -1439,36 +1439,29 @@ var Args = class _Args {
1439
1439
  }
1440
1440
  return;
1441
1441
  }
1442
- let dotEnvPathFile = null;
1443
- const envSpecificFile = `.env.${this.env}`;
1444
- const envSpecificPath = resolve(dotEnvPath, envSpecificFile);
1445
- if (existsSync(envSpecificPath)) {
1446
- dotEnvPathFile = envSpecificPath;
1447
- }
1448
- if (!dotEnvPathFile && !this.get("dotEnvPath")) {
1442
+ let dotEnvPathFile = resolve(dotEnvPath, dotEnvFile);
1443
+ if (!existsSync(dotEnvPathFile) && !this.get("dotEnvPath")) {
1449
1444
  const examplesPath = resolve(dotEnvPath, "examples");
1450
- const examplesEnvSpecificPath = resolve(examplesPath, envSpecificFile);
1451
- if (existsSync(examplesEnvSpecificPath)) {
1452
- dotEnvPathFile = examplesEnvSpecificPath;
1453
- }
1454
- }
1455
- if (!dotEnvPathFile) {
1456
- dotEnvPathFile = resolve(dotEnvPath, dotEnvFile);
1457
- if (!existsSync(dotEnvPathFile)) {
1458
- if (!this.get("dotEnvPath")) {
1459
- const examplesPath = resolve(dotEnvPath, "examples");
1460
- const examplesEnvFile = resolve(examplesPath, dotEnvFile);
1461
- if (existsSync(examplesEnvFile)) {
1462
- dotEnvPathFile = examplesEnvFile;
1463
- } else {
1464
- dotEnvPathFile = resolve(dotEnvPath, "..", dotEnvFile);
1465
- }
1466
- }
1445
+ const examplesEnvFile = resolve(examplesPath, dotEnvFile);
1446
+ if (existsSync(examplesEnvFile)) {
1447
+ dotEnvPathFile = examplesEnvFile;
1448
+ } else {
1449
+ dotEnvPathFile = resolve(dotEnvPath, "..", dotEnvFile);
1467
1450
  }
1468
1451
  }
1469
1452
  if (dotEnvPathFile && existsSync(dotEnvPathFile)) {
1470
1453
  config({ path: dotEnvPathFile, quiet: true });
1471
1454
  }
1455
+ const envSpecificFile = `.env.${this.env}`;
1456
+ const envSpecificPath = resolve(dotEnvPath, envSpecificFile);
1457
+ if (existsSync(envSpecificPath) && envSpecificPath !== dotEnvPathFile) {
1458
+ config({ path: envSpecificPath, quiet: true });
1459
+ } else if (!this.get("dotEnvPath")) {
1460
+ const examplesOverlay = resolve(dotEnvPath, "examples", envSpecificFile);
1461
+ if (existsSync(examplesOverlay)) {
1462
+ config({ path: examplesOverlay, quiet: true });
1463
+ }
1464
+ }
1472
1465
  }
1473
1466
  /**
1474
1467
  * Load configuration files
@@ -2788,10 +2781,35 @@ async function ensureSchema(db, spec, options = {}) {
2788
2781
  // src/db/index.js
2789
2782
  var KNEX_DEFAULTS = {
2790
2783
  testConnection: true,
2791
- pool: { min: 2, max: 10 },
2784
+ // min: 0 avoids holding idle sockets that go stale during long-running CLIs
2785
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
2792
2786
  acquireConnectionTimeout: 1e4,
2793
2787
  ssl: { rejectUnauthorized: false }
2794
2788
  };
2789
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
2790
+ "ECONNRESET",
2791
+ "ECONNREFUSED",
2792
+ "EPIPE",
2793
+ "ETIMEDOUT",
2794
+ "ENOTFOUND",
2795
+ "EHOSTUNREACH",
2796
+ "ENETUNREACH",
2797
+ "ECONNABORTED",
2798
+ "CONNECTION_ENDED",
2799
+ "CONNECTION_CLOSED",
2800
+ // PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
2801
+ "08000",
2802
+ "08001",
2803
+ "08003",
2804
+ "08004",
2805
+ "08006",
2806
+ "08007",
2807
+ "08P01",
2808
+ "57P01",
2809
+ "57P02",
2810
+ "57P03"
2811
+ ]);
2812
+ 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;
2795
2813
  var Db = class _Db {
2796
2814
  static async init(context, options = {}) {
2797
2815
  const buildConfig = async () => {
@@ -3039,10 +3057,11 @@ var Db = class _Db {
3039
3057
  this.knexInstance = null;
3040
3058
  this.isConnected = false;
3041
3059
  this.queriesLog = [];
3060
+ this._reconnectPromise = null;
3042
3061
  this.config = {
3043
3062
  testConnection: true,
3044
3063
  profile: false,
3045
- pool: { min: 2, max: 10 },
3064
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3046
3065
  acquireConnectionTimeout: 1e4,
3047
3066
  ssl: { rejectUnauthorized: false },
3048
3067
  logger: console,
@@ -3060,7 +3079,7 @@ var Db = class _Db {
3060
3079
  if (!inst.knexInstance) {
3061
3080
  throw new Error("Db: Not connected. Call connect() first.");
3062
3081
  }
3063
- return inst.knexInstance(...argumentsList);
3082
+ return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
3064
3083
  },
3065
3084
  get: (target, prop) => {
3066
3085
  if (prop === "_instance") {
@@ -3070,8 +3089,12 @@ var Db = class _Db {
3070
3089
  const ownMethods = [
3071
3090
  "connect",
3072
3091
  "disconnect",
3092
+ "reconnect",
3073
3093
  "testConnection",
3074
3094
  "tableExists",
3095
+ "raw",
3096
+ "withConnectionRetry",
3097
+ "isConnectionError",
3075
3098
  "getQueryLog",
3076
3099
  "getKnex",
3077
3100
  "isConnectedToDb",
@@ -3091,6 +3114,9 @@ var Db = class _Db {
3091
3114
  if (inst.knexInstance) {
3092
3115
  const knexProp = inst.knexInstance[prop];
3093
3116
  if (typeof knexProp === "function") {
3117
+ if (prop === "raw") {
3118
+ return (...args) => inst.raw(...args);
3119
+ }
3094
3120
  return knexProp.bind(inst.knexInstance);
3095
3121
  }
3096
3122
  return knexProp;
@@ -3170,6 +3196,130 @@ var Db = class _Db {
3170
3196
  throw error;
3171
3197
  }
3172
3198
  }
3199
+ /**
3200
+ * True when the error indicates a dead socket / pool that a fresh connect may fix.
3201
+ * Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
3202
+ */
3203
+ isConnectionError(error) {
3204
+ if (!error) {
3205
+ return false;
3206
+ }
3207
+ if (error instanceof AggregateError && Array.isArray(error.errors)) {
3208
+ return error.errors.some((e) => this.isConnectionError(e));
3209
+ }
3210
+ const code = error.code ?? error.errno;
3211
+ if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
3212
+ return true;
3213
+ }
3214
+ const msg = error instanceof Error ? error.message : String(error);
3215
+ return CONNECTION_ERROR_MESSAGE_RE.test(msg);
3216
+ }
3217
+ /**
3218
+ * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3219
+ */
3220
+ async reconnect() {
3221
+ if (this._reconnectPromise) {
3222
+ await this._reconnectPromise;
3223
+ return;
3224
+ }
3225
+ this._reconnectPromise = (async () => {
3226
+ const old = this.knexInstance;
3227
+ this.isConnected = false;
3228
+ this.knexInstance = null;
3229
+ this.queriesLog = [];
3230
+ if (old) {
3231
+ try {
3232
+ await old.destroy();
3233
+ } catch (error) {
3234
+ this.logger.debug?.(
3235
+ `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3236
+ );
3237
+ }
3238
+ }
3239
+ await this.connect();
3240
+ })();
3241
+ try {
3242
+ await this._reconnectPromise;
3243
+ } finally {
3244
+ this._reconnectPromise = null;
3245
+ }
3246
+ }
3247
+ async reconnectAfterConnectionError(error) {
3248
+ this.logger.warn?.(
3249
+ `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3250
+ );
3251
+ await this.reconnect();
3252
+ }
3253
+ /**
3254
+ * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
3255
+ * `fn` should look up `this.knexInstance` each call so the retry uses the new client.
3256
+ */
3257
+ async withConnectionRetry(fn, { retries = 1 } = {}) {
3258
+ let lastError;
3259
+ for (let attempt = 0; attempt <= retries; attempt++) {
3260
+ try {
3261
+ return await fn();
3262
+ } catch (error) {
3263
+ lastError = error;
3264
+ if (!this.isConnectionError(error) || attempt >= retries) {
3265
+ throw error;
3266
+ }
3267
+ await this.reconnectAfterConnectionError(error);
3268
+ }
3269
+ }
3270
+ throw lastError;
3271
+ }
3272
+ /**
3273
+ * Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
3274
+ * when the first attempt dies with a connection error.
3275
+ */
3276
+ wrapQueryBuilder(builder) {
3277
+ if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
3278
+ return builder;
3279
+ }
3280
+ builder.__dbReconnectWrapped = true;
3281
+ const inst = this;
3282
+ const protoThen = Object.getPrototypeOf(builder)?.then;
3283
+ if (typeof protoThen !== "function") {
3284
+ return builder;
3285
+ }
3286
+ builder.then = function(onFulfilled, onRejected) {
3287
+ const run = async () => {
3288
+ try {
3289
+ return await protoThen.call(builder);
3290
+ } catch (error) {
3291
+ if (!inst.isConnectionError(error)) {
3292
+ throw error;
3293
+ }
3294
+ await inst.reconnectAfterConnectionError(error);
3295
+ if (typeof builder.clone === "function") {
3296
+ const retry = builder.clone();
3297
+ retry.client = inst.knexInstance.client;
3298
+ return await protoThen.call(retry);
3299
+ }
3300
+ if (typeof builder.toSQL === "function" && inst.knexInstance) {
3301
+ const sql = builder.toSQL();
3302
+ const statements = Array.isArray(sql) ? sql : [sql];
3303
+ let last;
3304
+ for (const stmt of statements) {
3305
+ last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
3306
+ }
3307
+ return last;
3308
+ }
3309
+ throw error;
3310
+ }
3311
+ };
3312
+ return run().then(onFulfilled, onRejected);
3313
+ };
3314
+ return builder;
3315
+ }
3316
+ /** knex.raw with auto-reconnect on dead connections. */
3317
+ raw(...args) {
3318
+ if (!this.knexInstance) {
3319
+ throw new Error("Db: Not connected. Call connect() first.");
3320
+ }
3321
+ return this.wrapQueryBuilder(this.knexInstance.raw(...args));
3322
+ }
3173
3323
  getErrorMessage(error) {
3174
3324
  if (error instanceof AggregateError) {
3175
3325
  const errors = error.errors || [];
@@ -3340,7 +3490,9 @@ var Db = class _Db {
3340
3490
  throw new Error("Db: Not connected. Call connect() first.");
3341
3491
  }
3342
3492
  try {
3343
- return await this.knexInstance.schema.hasTable(tableName);
3493
+ return await this.withConnectionRetry(
3494
+ () => this.knexInstance.schema.hasTable(tableName)
3495
+ );
3344
3496
  } catch (error) {
3345
3497
  this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
3346
3498
  throw error;