@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.
@@ -1758,46 +1758,83 @@ var Params = class _Params {
1758
1758
  paramGetters = [];
1759
1759
  trackedParams = [];
1760
1760
  _currentModule = "script";
1761
- /** Resolved early in constructor so cleanup does not read params lazily */
1762
- _showUsedParams = false;
1761
+ /**
1762
+ * Resolved early in constructor so cleanup does not read params lazily.
1763
+ * One of: false (off) | "end" (print at exit) | "top" (print after init,
1764
+ * via context.showUsedParamsIfNeeded()).
1765
+ */
1766
+ _showUsedParamsMode = false;
1767
+ /** Guard so the dump prints at most once (top OR end, never both). */
1768
+ _usedParamsPrinted = false;
1763
1769
  constructor(context, options = {}) {
1764
1770
  this.context = context;
1765
1771
  this.args = context.args;
1766
1772
  if (Object.keys(options).length > 0) {
1767
1773
  this.configure(options);
1768
1774
  }
1769
- this._showUsedParams = this.get("showUsedParams", "boolean default false");
1775
+ this._resolveShowUsedParams();
1770
1776
  if (context && typeof context.registerCleanup === "function") {
1771
1777
  context.registerCleanup((ctx) => {
1772
- if (!ctx.params.getShowUsedParams()) return;
1773
- const byModule = ctx.params.getFiguredByModule();
1774
- const modules = Object.keys(byModule).sort();
1775
- if (modules.length === 0) return;
1776
- const logger = ctx.logger;
1777
- logger.debug("[Params]: list of used params:");
1778
- if (typeof logger.highlight !== "function") {
1779
- for (const mod of modules) {
1780
- logger.debug(` [${mod}]`);
1781
- for (const [key, entry] of Object.entries(byModule[mod])) {
1782
- logger.debug(` ${key}: ${JSON.stringify(entry.value)} (${entry.source})`);
1783
- }
1784
- }
1785
- return;
1786
- }
1787
- for (const mod of modules) {
1788
- logger.debug(` [${mod}]`);
1789
- for (const [key, entry] of Object.entries(byModule[mod])) {
1790
- const valueStr = JSON.stringify(entry.value);
1791
- const display = entry.source === "default" ? valueStr : logger.highlight(valueStr);
1792
- logger.debug(` ${key}: ${display} (${entry.source})`);
1793
- }
1794
- }
1778
+ if (!ctx.params.getShowUsedParamsMode()) return;
1779
+ ctx.params.printUsedParams(ctx.logger);
1795
1780
  });
1796
1781
  }
1797
1782
  }
1798
- /** Whether --showUsedParams was requested (resolved in constructor). */
1783
+ /**
1784
+ * Resolve the --showUsedParams mode. The flag is intentionally dual-typed:
1785
+ * (absent) / --no-showUsedParams / =false -> false (off)
1786
+ * --showUsedParams / =true -> "end" (print at exit)
1787
+ * --showUsedParams=top -> "top" (print after init)
1788
+ * Read raw (uncoerced) from args so the string "top" isn't forced to a
1789
+ * boolean, then track it under the "script" module for the dump itself.
1790
+ */
1791
+ _resolveShowUsedParams() {
1792
+ const raw = this.args.get("showUsedParams");
1793
+ const source = this.args.getSource?.("showUsedParams") ?? "default";
1794
+ let mode = false;
1795
+ if (raw === void 0 || raw === null) {
1796
+ mode = false;
1797
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
1798
+ mode = "top";
1799
+ } else {
1800
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
1801
+ const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
1802
+ mode = falsey ? false : "end";
1803
+ }
1804
+ this._showUsedParamsMode = mode;
1805
+ this.trackParam("showUsedParams", "string", mode, raw === void 0 ? "default" : source, "script");
1806
+ return mode;
1807
+ }
1808
+ /** Whether --showUsedParams was requested in any mode (truthy = on). */
1799
1809
  getShowUsedParams() {
1800
- return this._showUsedParams;
1810
+ return this._showUsedParamsMode !== false;
1811
+ }
1812
+ /** Resolved mode: false | "end" | "top". */
1813
+ getShowUsedParamsMode() {
1814
+ return this._showUsedParamsMode;
1815
+ }
1816
+ /**
1817
+ * Print the module-grouped list of figured params (the --showUsedParams
1818
+ * dump). Idempotent: only the first call prints, so callers can invoke it
1819
+ * at the top (long-running services) without double-printing at exit.
1820
+ */
1821
+ printUsedParams(logger) {
1822
+ if (this._usedParamsPrinted) return;
1823
+ const byModule = this.getFiguredByModule();
1824
+ const modules = Object.keys(byModule).sort();
1825
+ if (modules.length === 0) return;
1826
+ this._usedParamsPrinted = true;
1827
+ logger = logger ?? this.context?.logger ?? console;
1828
+ const hasHighlight = typeof logger.highlight === "function";
1829
+ logger.debug("[Params]: list of used params:");
1830
+ for (const mod of modules) {
1831
+ logger.debug(` [${mod}]`);
1832
+ for (const [key, entry] of Object.entries(byModule[mod])) {
1833
+ const valueStr = JSON.stringify(entry.value);
1834
+ const display = hasHighlight && entry.source !== "default" ? logger.highlight(valueStr) : valueStr;
1835
+ logger.debug(` ${key}: ${display} (${entry.source})`);
1836
+ }
1837
+ }
1801
1838
  }
1802
1839
  /**
1803
1840
  * Configure parameters
@@ -2065,6 +2102,18 @@ var Params = class _Params {
2065
2102
  this._currentModule = prev;
2066
2103
  }
2067
2104
  }
2105
+ /**
2106
+ * Async variant of {@link runWithModule} for modules that await params.get().
2107
+ */
2108
+ async runWithModuleAsync(moduleName, fn) {
2109
+ const prev = this._currentModule;
2110
+ this._currentModule = moduleName;
2111
+ try {
2112
+ return await fn();
2113
+ } finally {
2114
+ this._currentModule = prev;
2115
+ }
2116
+ }
2068
2117
  /**
2069
2118
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2070
2119
  */
@@ -2470,7 +2519,16 @@ function setup(opts = {}) {
2470
2519
  emitter: partialContext.emitter,
2471
2520
  isStop: partialContext.isStop,
2472
2521
  cleanupFunctions: partialContext.cleanupFunctions,
2473
- registerCleanup: partialContext.registerCleanup
2522
+ registerCleanup: partialContext.registerCleanup,
2523
+ // For long-running scripts (servers): with --showUsedParams=top, print
2524
+ // the used-params list now (after the script has initialized all its
2525
+ // own components), instead of at exit. No-op for the default mode,
2526
+ // which prints at exit via the cleanup registered by Params.
2527
+ showUsedParamsIfNeeded: () => {
2528
+ if (params.getShowUsedParamsMode?.() === "top") {
2529
+ params.printUsedParams(logger);
2530
+ }
2531
+ }
2474
2532
  };
2475
2533
  logger.debug("[setup] completed successfully");
2476
2534
  return context;
@@ -2537,15 +2595,19 @@ async function init(flow2, opts = {}) {
2537
2595
  process.exit(0);
2538
2596
  }
2539
2597
  let sigintCount = 0;
2598
+ let firstSigintAt = 0;
2540
2599
  process.on("SIGINT", async () => {
2541
2600
  if (!context) return;
2542
- sigintCount += 1;
2543
- if (sigintCount === 1) {
2601
+ const now = Date.now();
2602
+ if (sigintCount === 0) {
2603
+ sigintCount = 1;
2604
+ firstSigintAt = now;
2544
2605
  stop = true;
2545
2606
  context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2546
2607
  context.emitter.emit("stop", stopAllowance);
2547
2608
  return;
2548
2609
  }
2610
+ if (now - firstSigintAt < 250) return;
2549
2611
  context.logger.warn("[process] second SIGINT: running cleanup then exit");
2550
2612
  await runRegisteredCleanups(context);
2551
2613
  process.exit(2);
@@ -2593,38 +2655,71 @@ var KNEX_DEFAULTS = {
2593
2655
  };
2594
2656
  var Db = class {
2595
2657
  static async init(context, options = {}) {
2596
- const defs2 = {
2597
- dbName: "string",
2598
- dbConnectionString: "string",
2599
- dbProfile: "boolean default false"
2600
- };
2601
- const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2602
- const merged = { ...discovered, ...options };
2603
- let { dbName, dbConnectionString } = merged;
2604
- const { dbProfile } = merged;
2605
- if (!dbName && !dbConnectionString) {
2606
- dbName = "local";
2607
- }
2608
- if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2609
- dbConnectionString = dbName;
2610
- dbName = void 0;
2611
- }
2612
- if (dbName && !dbConnectionString) {
2613
- const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2614
- dbConnectionString = await context.params.get(paramName, "string");
2658
+ const buildConfig = async () => {
2659
+ const defs2 = {
2660
+ dbName: "string",
2661
+ dbProfile: "boolean default false"
2662
+ };
2663
+ const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
2664
+ const merged = { ...discovered, ...options };
2665
+ let { dbName, dbProfile } = merged;
2666
+ let dbConnectionString = options.dbConnectionString ?? options.connectionString;
2667
+ let connectionParam = dbConnectionString ? "options" : null;
2615
2668
  if (!dbConnectionString) {
2616
- throw new ParamError(
2617
- `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2618
- );
2669
+ const src = context?.args?.getSource?.("dbConnectionString");
2670
+ if (src === "cli" || src === "overrides" || src === "config") {
2671
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2672
+ connectionParam = "dbConnectionString";
2673
+ }
2619
2674
  }
2620
- }
2621
- const config2 = {
2622
- ...KNEX_DEFAULTS,
2623
- connectionString: dbConnectionString,
2624
- name: dbName || merged.name || "default",
2625
- profile: !!dbProfile,
2626
- logger: context.logger
2675
+ if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
2676
+ dbConnectionString = dbName;
2677
+ dbName = void 0;
2678
+ }
2679
+ if (!dbConnectionString && dbName) {
2680
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2681
+ dbConnectionString = await context.params.get(paramName, "string");
2682
+ connectionParam = paramName;
2683
+ if (!dbConnectionString) {
2684
+ throw new ParamError(
2685
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2686
+ );
2687
+ }
2688
+ }
2689
+ if (!dbConnectionString) {
2690
+ dbConnectionString = await context.params.get("dbConnectionString", "string");
2691
+ if (dbConnectionString) {
2692
+ connectionParam = "dbConnectionString";
2693
+ }
2694
+ }
2695
+ if (!dbConnectionString) {
2696
+ if (!dbName) {
2697
+ dbName = "local";
2698
+ }
2699
+ const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
2700
+ dbConnectionString = await context.params.get(paramName, "string");
2701
+ connectionParam = paramName;
2702
+ if (!dbConnectionString) {
2703
+ throw new ParamError(
2704
+ `Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
2705
+ );
2706
+ }
2707
+ }
2708
+ const displayName = resolveDbDisplayName(
2709
+ dbName,
2710
+ connectionParam,
2711
+ context?.args?.env,
2712
+ merged.name
2713
+ );
2714
+ return {
2715
+ ...KNEX_DEFAULTS,
2716
+ connectionString: dbConnectionString,
2717
+ name: displayName,
2718
+ profile: !!dbProfile,
2719
+ logger: context.logger
2720
+ };
2627
2721
  };
2722
+ const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
2628
2723
  return dbConnect(context, config2);
2629
2724
  }
2630
2725
  constructor(config2) {
@@ -2641,7 +2736,6 @@ var Db = class {
2641
2736
  acquireConnectionTimeout: 1e4,
2642
2737
  ssl: { rejectUnauthorized: false },
2643
2738
  logger: console,
2644
- name: "default",
2645
2739
  ...config2
2646
2740
  };
2647
2741
  this.logger = this.config.logger;
@@ -2741,9 +2835,7 @@ var Db = class {
2741
2835
  await this.testConnection();
2742
2836
  }
2743
2837
  this.isConnected = true;
2744
- this.logger.debug?.(
2745
- `[Db] Connected to database "${this.config.name || this.config.connectionString}"`
2746
- );
2838
+ this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
2747
2839
  } catch (error) {
2748
2840
  if (error instanceof ParamError) {
2749
2841
  throw error;
@@ -2761,9 +2853,7 @@ var Db = class {
2761
2853
  this.knexInstance = null;
2762
2854
  this.isConnected = false;
2763
2855
  this.queriesLog = [];
2764
- this.logger.debug?.(
2765
- `[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
2766
- );
2856
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
2767
2857
  } catch (error) {
2768
2858
  const errorMsg = this.getErrorMessage(error);
2769
2859
  this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
@@ -2892,15 +2982,74 @@ var Db = class {
2892
2982
  function capitalizeFirstLetter(str) {
2893
2983
  return str.charAt(0).toUpperCase() + str.slice(1);
2894
2984
  }
2985
+ function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
2986
+ if (dbName) {
2987
+ return dbName;
2988
+ }
2989
+ if (mergedName) {
2990
+ return mergedName;
2991
+ }
2992
+ if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
2993
+ return connectionParam.slice("dbConnectionString".length).toLowerCase();
2994
+ }
2995
+ if (connectionParam === "dbConnectionString" && argsEnv) {
2996
+ return argsEnv;
2997
+ }
2998
+ return void 0;
2999
+ }
3000
+ function formatDbConnectMessage(name, connectionString) {
3001
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3002
+ if (name) {
3003
+ return `[Db] Connected to database "${name}"${endpointSuffix}`;
3004
+ }
3005
+ return `[Db] Connected${endpointSuffix}`;
3006
+ }
3007
+ function formatDbDisconnectMessage(name, connectionString) {
3008
+ const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
3009
+ if (name) {
3010
+ return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
3011
+ }
3012
+ return `[Db] Disconnected${endpointSuffix}`;
3013
+ }
3014
+ function formatDbInstanceMessage(action, name) {
3015
+ if (name) {
3016
+ return `[Db] instance "${name}" ${action}`;
3017
+ }
3018
+ return `[Db] instance ${action}`;
3019
+ }
3020
+ function formatConnectionEndpointSuffix(connectionString) {
3021
+ const endpoint = formatConnectionEndpoint(connectionString);
3022
+ return endpoint ? ` (${endpoint})` : "";
3023
+ }
3024
+ function formatConnectionEndpoint(connectionString) {
3025
+ try {
3026
+ const url = new URL(connectionString);
3027
+ const host = url.hostname;
3028
+ if (!host) {
3029
+ return null;
3030
+ }
3031
+ let port = url.port;
3032
+ if (!port) {
3033
+ if (url.protocol === "postgresql:") {
3034
+ port = "5432";
3035
+ } else if (url.protocol === "mysql:") {
3036
+ port = "3306";
3037
+ }
3038
+ }
3039
+ return port ? `${host}:${port}` : host;
3040
+ } catch {
3041
+ return null;
3042
+ }
3043
+ }
2895
3044
  async function dbConnect(context, config2) {
2896
3045
  try {
2897
3046
  const db = new Db(config2);
2898
3047
  context.registerCleanup(async () => {
2899
3048
  await db.disconnect();
2900
- context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
3049
+ context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
2901
3050
  });
2902
3051
  await db.connect();
2903
- context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
3052
+ context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
2904
3053
  return db;
2905
3054
  } catch (error) {
2906
3055
  if (error instanceof ParamError) {