@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.
package/dist/index.js CHANGED
@@ -1388,36 +1388,29 @@ var Args = class _Args {
1388
1388
  }
1389
1389
  return;
1390
1390
  }
1391
- let dotEnvPathFile = null;
1392
- const envSpecificFile = `.env.${this.env}`;
1393
- const envSpecificPath = resolve(dotEnvPath, envSpecificFile);
1394
- if (existsSync(envSpecificPath)) {
1395
- dotEnvPathFile = envSpecificPath;
1396
- }
1397
- if (!dotEnvPathFile && !this.get("dotEnvPath")) {
1391
+ let dotEnvPathFile = resolve(dotEnvPath, dotEnvFile);
1392
+ if (!existsSync(dotEnvPathFile) && !this.get("dotEnvPath")) {
1398
1393
  const examplesPath = resolve(dotEnvPath, "examples");
1399
- const examplesEnvSpecificPath = resolve(examplesPath, envSpecificFile);
1400
- if (existsSync(examplesEnvSpecificPath)) {
1401
- dotEnvPathFile = examplesEnvSpecificPath;
1402
- }
1403
- }
1404
- if (!dotEnvPathFile) {
1405
- dotEnvPathFile = resolve(dotEnvPath, dotEnvFile);
1406
- if (!existsSync(dotEnvPathFile)) {
1407
- if (!this.get("dotEnvPath")) {
1408
- const examplesPath = resolve(dotEnvPath, "examples");
1409
- const examplesEnvFile = resolve(examplesPath, dotEnvFile);
1410
- if (existsSync(examplesEnvFile)) {
1411
- dotEnvPathFile = examplesEnvFile;
1412
- } else {
1413
- dotEnvPathFile = resolve(dotEnvPath, "..", dotEnvFile);
1414
- }
1415
- }
1394
+ const examplesEnvFile = resolve(examplesPath, dotEnvFile);
1395
+ if (existsSync(examplesEnvFile)) {
1396
+ dotEnvPathFile = examplesEnvFile;
1397
+ } else {
1398
+ dotEnvPathFile = resolve(dotEnvPath, "..", dotEnvFile);
1416
1399
  }
1417
1400
  }
1418
1401
  if (dotEnvPathFile && existsSync(dotEnvPathFile)) {
1419
1402
  config({ path: dotEnvPathFile, quiet: true });
1420
1403
  }
1404
+ const envSpecificFile = `.env.${this.env}`;
1405
+ const envSpecificPath = resolve(dotEnvPath, envSpecificFile);
1406
+ if (existsSync(envSpecificPath) && envSpecificPath !== dotEnvPathFile) {
1407
+ config({ path: envSpecificPath, quiet: true });
1408
+ } else if (!this.get("dotEnvPath")) {
1409
+ const examplesOverlay = resolve(dotEnvPath, "examples", envSpecificFile);
1410
+ if (existsSync(examplesOverlay)) {
1411
+ config({ path: examplesOverlay, quiet: true });
1412
+ }
1413
+ }
1421
1414
  }
1422
1415
  /**
1423
1416
  * Load configuration files
@@ -3478,10 +3471,35 @@ async function ensureSchemaEverywhere(dbs, spec, options = {}) {
3478
3471
  // src/db/index.js
3479
3472
  var KNEX_DEFAULTS = {
3480
3473
  testConnection: true,
3481
- pool: { min: 2, max: 10 },
3474
+ // min: 0 avoids holding idle sockets that go stale during long-running CLIs
3475
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3482
3476
  acquireConnectionTimeout: 1e4,
3483
3477
  ssl: { rejectUnauthorized: false }
3484
3478
  };
3479
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
3480
+ "ECONNRESET",
3481
+ "ECONNREFUSED",
3482
+ "EPIPE",
3483
+ "ETIMEDOUT",
3484
+ "ENOTFOUND",
3485
+ "EHOSTUNREACH",
3486
+ "ENETUNREACH",
3487
+ "ECONNABORTED",
3488
+ "CONNECTION_ENDED",
3489
+ "CONNECTION_CLOSED",
3490
+ // PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
3491
+ "08000",
3492
+ "08001",
3493
+ "08003",
3494
+ "08004",
3495
+ "08006",
3496
+ "08007",
3497
+ "08P01",
3498
+ "57P01",
3499
+ "57P02",
3500
+ "57P03"
3501
+ ]);
3502
+ 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;
3485
3503
  var Db = class _Db {
3486
3504
  static async init(context, options = {}) {
3487
3505
  const buildConfig = async () => {
@@ -3729,10 +3747,11 @@ var Db = class _Db {
3729
3747
  this.knexInstance = null;
3730
3748
  this.isConnected = false;
3731
3749
  this.queriesLog = [];
3750
+ this._reconnectPromise = null;
3732
3751
  this.config = {
3733
3752
  testConnection: true,
3734
3753
  profile: false,
3735
- pool: { min: 2, max: 10 },
3754
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3736
3755
  acquireConnectionTimeout: 1e4,
3737
3756
  ssl: { rejectUnauthorized: false },
3738
3757
  logger: console,
@@ -3750,7 +3769,7 @@ var Db = class _Db {
3750
3769
  if (!inst.knexInstance) {
3751
3770
  throw new Error("Db: Not connected. Call connect() first.");
3752
3771
  }
3753
- return inst.knexInstance(...argumentsList);
3772
+ return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
3754
3773
  },
3755
3774
  get: (target, prop) => {
3756
3775
  if (prop === "_instance") {
@@ -3760,8 +3779,12 @@ var Db = class _Db {
3760
3779
  const ownMethods = [
3761
3780
  "connect",
3762
3781
  "disconnect",
3782
+ "reconnect",
3763
3783
  "testConnection",
3764
3784
  "tableExists",
3785
+ "raw",
3786
+ "withConnectionRetry",
3787
+ "isConnectionError",
3765
3788
  "getQueryLog",
3766
3789
  "getKnex",
3767
3790
  "isConnectedToDb",
@@ -3781,6 +3804,9 @@ var Db = class _Db {
3781
3804
  if (inst.knexInstance) {
3782
3805
  const knexProp = inst.knexInstance[prop];
3783
3806
  if (typeof knexProp === "function") {
3807
+ if (prop === "raw") {
3808
+ return (...args) => inst.raw(...args);
3809
+ }
3784
3810
  return knexProp.bind(inst.knexInstance);
3785
3811
  }
3786
3812
  return knexProp;
@@ -3860,6 +3886,130 @@ var Db = class _Db {
3860
3886
  throw error;
3861
3887
  }
3862
3888
  }
3889
+ /**
3890
+ * True when the error indicates a dead socket / pool that a fresh connect may fix.
3891
+ * Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
3892
+ */
3893
+ isConnectionError(error) {
3894
+ if (!error) {
3895
+ return false;
3896
+ }
3897
+ if (error instanceof AggregateError && Array.isArray(error.errors)) {
3898
+ return error.errors.some((e) => this.isConnectionError(e));
3899
+ }
3900
+ const code = error.code ?? error.errno;
3901
+ if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
3902
+ return true;
3903
+ }
3904
+ const msg = error instanceof Error ? error.message : String(error);
3905
+ return CONNECTION_ERROR_MESSAGE_RE.test(msg);
3906
+ }
3907
+ /**
3908
+ * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3909
+ */
3910
+ async reconnect() {
3911
+ if (this._reconnectPromise) {
3912
+ await this._reconnectPromise;
3913
+ return;
3914
+ }
3915
+ this._reconnectPromise = (async () => {
3916
+ const old = this.knexInstance;
3917
+ this.isConnected = false;
3918
+ this.knexInstance = null;
3919
+ this.queriesLog = [];
3920
+ if (old) {
3921
+ try {
3922
+ await old.destroy();
3923
+ } catch (error) {
3924
+ this.logger.debug?.(
3925
+ `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3926
+ );
3927
+ }
3928
+ }
3929
+ await this.connect();
3930
+ })();
3931
+ try {
3932
+ await this._reconnectPromise;
3933
+ } finally {
3934
+ this._reconnectPromise = null;
3935
+ }
3936
+ }
3937
+ async reconnectAfterConnectionError(error) {
3938
+ this.logger.warn?.(
3939
+ `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3940
+ );
3941
+ await this.reconnect();
3942
+ }
3943
+ /**
3944
+ * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
3945
+ * `fn` should look up `this.knexInstance` each call so the retry uses the new client.
3946
+ */
3947
+ async withConnectionRetry(fn, { retries = 1 } = {}) {
3948
+ let lastError;
3949
+ for (let attempt = 0; attempt <= retries; attempt++) {
3950
+ try {
3951
+ return await fn();
3952
+ } catch (error) {
3953
+ lastError = error;
3954
+ if (!this.isConnectionError(error) || attempt >= retries) {
3955
+ throw error;
3956
+ }
3957
+ await this.reconnectAfterConnectionError(error);
3958
+ }
3959
+ }
3960
+ throw lastError;
3961
+ }
3962
+ /**
3963
+ * Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
3964
+ * when the first attempt dies with a connection error.
3965
+ */
3966
+ wrapQueryBuilder(builder) {
3967
+ if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
3968
+ return builder;
3969
+ }
3970
+ builder.__dbReconnectWrapped = true;
3971
+ const inst = this;
3972
+ const protoThen = Object.getPrototypeOf(builder)?.then;
3973
+ if (typeof protoThen !== "function") {
3974
+ return builder;
3975
+ }
3976
+ builder.then = function(onFulfilled, onRejected) {
3977
+ const run2 = async () => {
3978
+ try {
3979
+ return await protoThen.call(builder);
3980
+ } catch (error) {
3981
+ if (!inst.isConnectionError(error)) {
3982
+ throw error;
3983
+ }
3984
+ await inst.reconnectAfterConnectionError(error);
3985
+ if (typeof builder.clone === "function") {
3986
+ const retry = builder.clone();
3987
+ retry.client = inst.knexInstance.client;
3988
+ return await protoThen.call(retry);
3989
+ }
3990
+ if (typeof builder.toSQL === "function" && inst.knexInstance) {
3991
+ const sql = builder.toSQL();
3992
+ const statements = Array.isArray(sql) ? sql : [sql];
3993
+ let last;
3994
+ for (const stmt of statements) {
3995
+ last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
3996
+ }
3997
+ return last;
3998
+ }
3999
+ throw error;
4000
+ }
4001
+ };
4002
+ return run2().then(onFulfilled, onRejected);
4003
+ };
4004
+ return builder;
4005
+ }
4006
+ /** knex.raw with auto-reconnect on dead connections. */
4007
+ raw(...args) {
4008
+ if (!this.knexInstance) {
4009
+ throw new Error("Db: Not connected. Call connect() first.");
4010
+ }
4011
+ return this.wrapQueryBuilder(this.knexInstance.raw(...args));
4012
+ }
3863
4013
  getErrorMessage(error) {
3864
4014
  if (error instanceof AggregateError) {
3865
4015
  const errors = error.errors || [];
@@ -4030,7 +4180,9 @@ var Db = class _Db {
4030
4180
  throw new Error("Db: Not connected. Call connect() first.");
4031
4181
  }
4032
4182
  try {
4033
- return await this.knexInstance.schema.hasTable(tableName);
4183
+ return await this.withConnectionRetry(
4184
+ () => this.knexInstance.schema.hasTable(tableName)
4185
+ );
4034
4186
  } catch (error) {
4035
4187
  this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
4036
4188
  throw error;
@@ -5665,6 +5817,8 @@ module.exports = {
5665
5817
  merge_logs: true,
5666
5818
  time: true,
5667
5819
  env: {
5820
+ // cli-toolkit Args: ENV selects env-suffixed defaults (DB_NAME_PRODUCTION, BUCKET_PRODUCTION, \u2026)
5821
+ ENV: "production",
5668
5822
  NODE_ENV: "production",
5669
5823
  },
5670
5824
  },