@nmakarov/cli-toolkit 0.23.0 → 0.27.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.
@@ -1742,46 +1742,83 @@ var Params = class _Params {
1742
1742
  paramGetters = [];
1743
1743
  trackedParams = [];
1744
1744
  _currentModule = "script";
1745
- /** Resolved early in constructor so cleanup does not read params lazily */
1746
- _showUsedParams = false;
1745
+ /**
1746
+ * Resolved early in constructor so cleanup does not read params lazily.
1747
+ * One of: false (off) | "end" (print at exit) | "top" (print after init,
1748
+ * via context.showUsedParamsIfNeeded()).
1749
+ */
1750
+ _showUsedParamsMode = false;
1751
+ /** Guard so the dump prints at most once (top OR end, never both). */
1752
+ _usedParamsPrinted = false;
1747
1753
  constructor(context, options = {}) {
1748
1754
  this.context = context;
1749
1755
  this.args = context.args;
1750
1756
  if (Object.keys(options).length > 0) {
1751
1757
  this.configure(options);
1752
1758
  }
1753
- this._showUsedParams = this.get("showUsedParams", "boolean default false");
1759
+ this._resolveShowUsedParams();
1754
1760
  if (context && typeof context.registerCleanup === "function") {
1755
1761
  context.registerCleanup((ctx) => {
1756
- if (!ctx.params.getShowUsedParams()) return;
1757
- const byModule = ctx.params.getFiguredByModule();
1758
- const modules = Object.keys(byModule).sort();
1759
- if (modules.length === 0) return;
1760
- const logger = ctx.logger;
1761
- logger.debug("[Params]: list of used params:");
1762
- if (typeof logger.highlight !== "function") {
1763
- for (const mod of modules) {
1764
- logger.debug(` [${mod}]`);
1765
- for (const [key, entry] of Object.entries(byModule[mod])) {
1766
- logger.debug(` ${key}: ${JSON.stringify(entry.value)} (${entry.source})`);
1767
- }
1768
- }
1769
- return;
1770
- }
1771
- for (const mod of modules) {
1772
- logger.debug(` [${mod}]`);
1773
- for (const [key, entry] of Object.entries(byModule[mod])) {
1774
- const valueStr = JSON.stringify(entry.value);
1775
- const display = entry.source === "default" ? valueStr : logger.highlight(valueStr);
1776
- logger.debug(` ${key}: ${display} (${entry.source})`);
1777
- }
1778
- }
1762
+ if (!ctx.params.getShowUsedParamsMode()) return;
1763
+ ctx.params.printUsedParams(ctx.logger);
1779
1764
  });
1780
1765
  }
1781
1766
  }
1782
- /** Whether --showUsedParams was requested (resolved in constructor). */
1767
+ /**
1768
+ * Resolve the --showUsedParams mode. The flag is intentionally dual-typed:
1769
+ * (absent) / --no-showUsedParams / =false -> false (off)
1770
+ * --showUsedParams / =true -> "end" (print at exit)
1771
+ * --showUsedParams=top -> "top" (print after init)
1772
+ * Read raw (uncoerced) from args so the string "top" isn't forced to a
1773
+ * boolean, then track it under the "script" module for the dump itself.
1774
+ */
1775
+ _resolveShowUsedParams() {
1776
+ const raw = this.args.get("showUsedParams");
1777
+ const source = this.args.getSource?.("showUsedParams") ?? "default";
1778
+ let mode = false;
1779
+ if (raw === void 0 || raw === null) {
1780
+ mode = false;
1781
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
1782
+ mode = "top";
1783
+ } else {
1784
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
1785
+ const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
1786
+ mode = falsey ? false : "end";
1787
+ }
1788
+ this._showUsedParamsMode = mode;
1789
+ this.trackParam("showUsedParams", "string", mode, raw === void 0 ? "default" : source, "script");
1790
+ return mode;
1791
+ }
1792
+ /** Whether --showUsedParams was requested in any mode (truthy = on). */
1783
1793
  getShowUsedParams() {
1784
- return this._showUsedParams;
1794
+ return this._showUsedParamsMode !== false;
1795
+ }
1796
+ /** Resolved mode: false | "end" | "top". */
1797
+ getShowUsedParamsMode() {
1798
+ return this._showUsedParamsMode;
1799
+ }
1800
+ /**
1801
+ * Print the module-grouped list of figured params (the --showUsedParams
1802
+ * dump). Idempotent: only the first call prints, so callers can invoke it
1803
+ * at the top (long-running services) without double-printing at exit.
1804
+ */
1805
+ printUsedParams(logger) {
1806
+ if (this._usedParamsPrinted) return;
1807
+ const byModule = this.getFiguredByModule();
1808
+ const modules = Object.keys(byModule).sort();
1809
+ if (modules.length === 0) return;
1810
+ this._usedParamsPrinted = true;
1811
+ logger = logger ?? this.context?.logger ?? console;
1812
+ const hasHighlight = typeof logger.highlight === "function";
1813
+ logger.debug("[Params]: list of used params:");
1814
+ for (const mod of modules) {
1815
+ logger.debug(` [${mod}]`);
1816
+ for (const [key, entry] of Object.entries(byModule[mod])) {
1817
+ const valueStr = JSON.stringify(entry.value);
1818
+ const display = hasHighlight && entry.source !== "default" ? logger.highlight(valueStr) : valueStr;
1819
+ logger.debug(` ${key}: ${display} (${entry.source})`);
1820
+ }
1821
+ }
1785
1822
  }
1786
1823
  /**
1787
1824
  * Configure parameters
@@ -2049,6 +2086,18 @@ var Params = class _Params {
2049
2086
  this._currentModule = prev;
2050
2087
  }
2051
2088
  }
2089
+ /**
2090
+ * Async variant of {@link runWithModule} for modules that await params.get().
2091
+ */
2092
+ async runWithModuleAsync(moduleName, fn) {
2093
+ const prev = this._currentModule;
2094
+ this._currentModule = moduleName;
2095
+ try {
2096
+ return await fn();
2097
+ } finally {
2098
+ this._currentModule = prev;
2099
+ }
2100
+ }
2052
2101
  /**
2053
2102
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2054
2103
  */
@@ -2454,7 +2503,16 @@ function setup(opts = {}) {
2454
2503
  emitter: partialContext.emitter,
2455
2504
  isStop: partialContext.isStop,
2456
2505
  cleanupFunctions: partialContext.cleanupFunctions,
2457
- registerCleanup: partialContext.registerCleanup
2506
+ registerCleanup: partialContext.registerCleanup,
2507
+ // For long-running scripts (servers): with --showUsedParams=top, print
2508
+ // the used-params list now (after the script has initialized all its
2509
+ // own components), instead of at exit. No-op for the default mode,
2510
+ // which prints at exit via the cleanup registered by Params.
2511
+ showUsedParamsIfNeeded: () => {
2512
+ if (params.getShowUsedParamsMode?.() === "top") {
2513
+ params.printUsedParams(logger);
2514
+ }
2515
+ }
2458
2516
  };
2459
2517
  logger.debug("[setup] completed successfully");
2460
2518
  return context;
@@ -2521,15 +2579,19 @@ async function init(flow2, opts = {}) {
2521
2579
  process.exit(0);
2522
2580
  }
2523
2581
  let sigintCount = 0;
2582
+ let firstSigintAt = 0;
2524
2583
  process.on("SIGINT", async () => {
2525
2584
  if (!context) return;
2526
- sigintCount += 1;
2527
- if (sigintCount === 1) {
2585
+ const now = Date.now();
2586
+ if (sigintCount === 0) {
2587
+ sigintCount = 1;
2588
+ firstSigintAt = now;
2528
2589
  stop = true;
2529
2590
  context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2530
2591
  context.emitter.emit("stop", stopAllowance);
2531
2592
  return;
2532
2593
  }
2594
+ if (now - firstSigintAt < 250) return;
2533
2595
  context.logger.warn("[process] second SIGINT: running cleanup then exit");
2534
2596
  await runRegisteredCleanups(context);
2535
2597
  process.exit(2);
@@ -2577,38 +2639,71 @@ var KNEX_DEFAULTS = {
2577
2639
  };
2578
2640
  var Db = class {
2579
2641
  static async init(context, options = {}) {
2580
- const defs2 = {
2581
- dbName: "string",
2582
- dbConnectionString: "string",
2583
- dbProfile: "boolean default false"
2584
- };
2585
- const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2586
- const merged = { ...discovered, ...options };
2587
- let { dbName, dbConnectionString } = merged;
2588
- const { dbProfile } = merged;
2589
- if (!dbName && !dbConnectionString) {
2590
- dbName = "local";
2591
- }
2592
- if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2593
- dbConnectionString = dbName;
2594
- dbName = void 0;
2595
- }
2596
- if (dbName && !dbConnectionString) {
2597
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2598
- dbConnectionString = await context.params.get(paramName, "string");
2642
+ const buildConfig = async () => {
2643
+ const defs2 = {
2644
+ dbName: "string",
2645
+ dbProfile: "boolean default false"
2646
+ };
2647
+ const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2648
+ const merged = { ...discovered, ...options };
2649
+ let { dbName, dbProfile } = merged;
2650
+ let dbConnectionString = options.dbConnectionString ?? options.connectionString;
2651
+ let connectionParam = dbConnectionString ? "options" : null;
2599
2652
  if (!dbConnectionString) {
2600
- throw new ParamError(
2601
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2602
- );
2653
+ const src = context?.args?.getSource?.("dbConnectionString");
2654
+ if (src === "cli" || src === "overrides" || src === "config") {
2655
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2656
+ connectionParam = "dbConnectionString";
2657
+ }
2603
2658
  }
2604
- }
2605
- const config2 = {
2606
- ...KNEX_DEFAULTS,
2607
- connectionString: dbConnectionString,
2608
- name: dbName || merged.name || "default",
2609
- profile: !!dbProfile,
2610
- logger: context.logger
2659
+ if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2660
+ dbConnectionString = dbName;
2661
+ dbName = void 0;
2662
+ }
2663
+ if (!dbConnectionString && dbName) {
2664
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2665
+ dbConnectionString = await context.params.get(paramName, "string");
2666
+ connectionParam = paramName;
2667
+ if (!dbConnectionString) {
2668
+ throw new ParamError(
2669
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2670
+ );
2671
+ }
2672
+ }
2673
+ if (!dbConnectionString) {
2674
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2675
+ if (dbConnectionString) {
2676
+ connectionParam = "dbConnectionString";
2677
+ }
2678
+ }
2679
+ if (!dbConnectionString) {
2680
+ if (!dbName) {
2681
+ dbName = "local";
2682
+ }
2683
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2684
+ dbConnectionString = await context.params.get(paramName, "string");
2685
+ connectionParam = paramName;
2686
+ if (!dbConnectionString) {
2687
+ throw new ParamError(
2688
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2689
+ );
2690
+ }
2691
+ }
2692
+ const displayName = resolveDbDisplayName(
2693
+ dbName,
2694
+ connectionParam,
2695
+ context?.args?.env,
2696
+ merged.name
2697
+ );
2698
+ return {
2699
+ ...KNEX_DEFAULTS,
2700
+ connectionString: dbConnectionString,
2701
+ name: displayName,
2702
+ profile: !!dbProfile,
2703
+ logger: context.logger
2704
+ };
2611
2705
  };
2706
+ const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
2612
2707
  return dbConnect(context, config2);
2613
2708
  }
2614
2709
  constructor(config2) {
@@ -2625,7 +2720,6 @@ var Db = class {
2625
2720
  acquireConnectionTimeout: 1e4,
2626
2721
  ssl: { rejectUnauthorized: false },
2627
2722
  logger: console,
2628
- name: "default",
2629
2723
  ...config2
2630
2724
  };
2631
2725
  this.logger = this.config.logger;
@@ -2725,9 +2819,7 @@ var Db = class {
2725
2819
  await this.testConnection();
2726
2820
  }
2727
2821
  this.isConnected = true;
2728
- this.logger.debug?.(
2729
- `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
2730
- );
2822
+ this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
2731
2823
  } catch (error) {
2732
2824
  if (error instanceof ParamError) {
2733
2825
  throw error;
@@ -2745,9 +2837,7 @@ var Db = class {
2745
2837
  this.knexInstance = null;
2746
2838
  this.isConnected = false;
2747
2839
  this.queriesLog = [];
2748
- this.logger.debug?.(
2749
- `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
2750
- );
2840
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
2751
2841
  } catch (error) {
2752
2842
  const errorMsg = this.getErrorMessage(error);
2753
2843
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
@@ -2876,15 +2966,74 @@ var Db = class {
2876
2966
  function capitalizeFirstLetter(str) {
2877
2967
  return str.charAt(0).toUpperCase() + str.slice(1);
2878
2968
  }
2969
+ function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
2970
+ if (dbName) {
2971
+ return dbName;
2972
+ }
2973
+ if (mergedName) {
2974
+ return mergedName;
2975
+ }
2976
+ if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
2977
+ return connectionParam.slice("dbConnectionString".length).toLowerCase();
2978
+ }
2979
+ if (connectionParam === "dbConnectionString" && argsEnv) {
2980
+ return argsEnv;
2981
+ }
2982
+ return void 0;
2983
+ }
2984
+ function formatDbConnectMessage(name, connectionString) {
2985
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
2986
+ if (name) {
2987
+ return `[Db] Connected to database "${name}"${endpointSuffix}`;
2988
+ }
2989
+ return `[Db] Connected${endpointSuffix}`;
2990
+ }
2991
+ function formatDbDisconnectMessage(name, connectionString) {
2992
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
2993
+ if (name) {
2994
+ return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
2995
+ }
2996
+ return `[Db] Disconnected${endpointSuffix}`;
2997
+ }
2998
+ function formatDbInstanceMessage(action, name) {
2999
+ if (name) {
3000
+ return `[Db] instance "${name}" ${action}`;
3001
+ }
3002
+ return `[Db] instance ${action}`;
3003
+ }
3004
+ function formatConnectionEndpointSuffix(connectionString) {
3005
+ const endpoint = formatConnectionEndpoint(connectionString);
3006
+ return endpoint ? ` (${endpoint})` : "";
3007
+ }
3008
+ function formatConnectionEndpoint(connectionString) {
3009
+ try {
3010
+ const url = new URL(connectionString);
3011
+ const host = url.hostname;
3012
+ if (!host) {
3013
+ return null;
3014
+ }
3015
+ let port = url.port;
3016
+ if (!port) {
3017
+ if (url.protocol === "postgresql:") {
3018
+ port = "5432";
3019
+ } else if (url.protocol === "mysql:") {
3020
+ port = "3306";
3021
+ }
3022
+ }
3023
+ return port ? `${host}:${port}` : host;
3024
+ } catch {
3025
+ return null;
3026
+ }
3027
+ }
2879
3028
  async function dbConnect(context, config2) {
2880
3029
  try {
2881
3030
  const db = new Db(config2);
2882
3031
  context.registerCleanup(async () => {
2883
3032
  await db.disconnect();
2884
- context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
3033
+ context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
2885
3034
  });
2886
3035
  await db.connect();
2887
- context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
3036
+ context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
2888
3037
  return db;
2889
3038
  } catch (error) {
2890
3039
  if (error instanceof ParamError) {