@nmakarov/cli-toolkit 0.25.0 → 0.29.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,87 @@ 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()) | "stop" (print after init, then
1749
+ * exit the process — also via context.showUsedParamsIfNeeded()).
1750
+ */
1751
+ _showUsedParamsMode = false;
1752
+ /** Guard so the dump prints at most once (top OR end, never both). */
1753
+ _usedParamsPrinted = false;
1747
1754
  constructor(context, options = {}) {
1748
1755
  this.context = context;
1749
1756
  this.args = context.args;
1750
1757
  if (Object.keys(options).length > 0) {
1751
1758
  this.configure(options);
1752
1759
  }
1753
- this._showUsedParams = this.get("showUsedParams", "boolean default false");
1760
+ this._resolveShowUsedParams();
1754
1761
  if (context && typeof context.registerCleanup === "function") {
1755
1762
  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
- }
1763
+ if (!ctx.params.getShowUsedParamsMode()) return;
1764
+ ctx.params.printUsedParams(ctx.logger);
1779
1765
  });
1780
1766
  }
1781
1767
  }
1782
- /** Whether --showUsedParams was requested (resolved in constructor). */
1768
+ /**
1769
+ * Resolve the --showUsedParams mode. The flag is intentionally dual-typed:
1770
+ * (absent) / --no-showUsedParams / =false -> false (off)
1771
+ * --showUsedParams / =true -> "end" (print at exit)
1772
+ * --showUsedParams=top -> "top" (print after init)
1773
+ * --showUsedParams=stop -> "stop" (print after init, then exit)
1774
+ * Read raw (uncoerced) from args so the string "top"/"stop" isn't forced to
1775
+ * a boolean, then track it under the "script" module for the dump itself.
1776
+ */
1777
+ _resolveShowUsedParams() {
1778
+ const raw = this.args.get("showUsedParams");
1779
+ const source = this.args.getSource?.("showUsedParams") ?? "default";
1780
+ let mode = false;
1781
+ if (raw === void 0 || raw === null) {
1782
+ mode = false;
1783
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
1784
+ mode = "top";
1785
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "stop") {
1786
+ mode = "stop";
1787
+ } else {
1788
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
1789
+ const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
1790
+ mode = falsey ? false : "end";
1791
+ }
1792
+ this._showUsedParamsMode = mode;
1793
+ this.trackParam("showUsedParams", "string", mode, raw === void 0 ? "default" : source, "script");
1794
+ return mode;
1795
+ }
1796
+ /** Whether --showUsedParams was requested in any mode (truthy = on). */
1783
1797
  getShowUsedParams() {
1784
- return this._showUsedParams;
1798
+ return this._showUsedParamsMode !== false;
1799
+ }
1800
+ /** Resolved mode: false | "end" | "top" | "stop". */
1801
+ getShowUsedParamsMode() {
1802
+ return this._showUsedParamsMode;
1803
+ }
1804
+ /**
1805
+ * Print the module-grouped list of figured params (the --showUsedParams
1806
+ * dump). Idempotent: only the first call prints, so callers can invoke it
1807
+ * at the top (long-running services) without double-printing at exit.
1808
+ */
1809
+ printUsedParams(logger) {
1810
+ if (this._usedParamsPrinted) return;
1811
+ const byModule = this.getFiguredByModule();
1812
+ const modules = Object.keys(byModule).sort();
1813
+ if (modules.length === 0) return;
1814
+ this._usedParamsPrinted = true;
1815
+ logger = logger ?? this.context?.logger ?? console;
1816
+ const hasHighlight = typeof logger.highlight === "function";
1817
+ logger.debug("[Params]: list of used params:");
1818
+ for (const mod of modules) {
1819
+ logger.debug(` [${mod}]`);
1820
+ for (const [key, entry] of Object.entries(byModule[mod])) {
1821
+ const valueStr = JSON.stringify(entry.value);
1822
+ const display = hasHighlight && entry.source !== "default" ? logger.highlight(valueStr) : valueStr;
1823
+ logger.debug(` ${key}: ${display} (${entry.source})`);
1824
+ }
1825
+ }
1785
1826
  }
1786
1827
  /**
1787
1828
  * Configure parameters
@@ -2466,7 +2507,25 @@ function setup(opts = {}) {
2466
2507
  emitter: partialContext.emitter,
2467
2508
  isStop: partialContext.isStop,
2468
2509
  cleanupFunctions: partialContext.cleanupFunctions,
2469
- registerCleanup: partialContext.registerCleanup
2510
+ registerCleanup: partialContext.registerCleanup,
2511
+ // For long-running scripts (servers): with --showUsedParams=top, print
2512
+ // the used-params list now (after the script has initialized all its
2513
+ // own components), instead of at exit. No-op for the default mode,
2514
+ // which prints at exit via the cleanup registered by Params.
2515
+ //
2516
+ // --showUsedParams=stop behaves like "top" but then exits immediately —
2517
+ // a quick "show me the figured params and quit" that skips the flow's
2518
+ // actual work. Like --stopAfter=init, this is a hard exit(0) (registered
2519
+ // cleanups are skipped); call it once components/params are resolved.
2520
+ showUsedParamsIfNeeded: () => {
2521
+ const mode = params.getShowUsedParamsMode?.();
2522
+ if (mode !== "top" && mode !== "stop") return;
2523
+ params.printUsedParams(logger);
2524
+ if (mode === "stop") {
2525
+ logger.debug?.("[showUsedParams=stop] params printed \u2014 exiting");
2526
+ process.exit(0);
2527
+ }
2528
+ }
2470
2529
  };
2471
2530
  logger.debug("[setup] completed successfully");
2472
2531
  return context;
@@ -2533,15 +2592,19 @@ async function init(flow2, opts = {}) {
2533
2592
  process.exit(0);
2534
2593
  }
2535
2594
  let sigintCount = 0;
2595
+ let firstSigintAt = 0;
2536
2596
  process.on("SIGINT", async () => {
2537
2597
  if (!context) return;
2538
- sigintCount += 1;
2539
- if (sigintCount === 1) {
2598
+ const now = Date.now();
2599
+ if (sigintCount === 0) {
2600
+ sigintCount = 1;
2601
+ firstSigintAt = now;
2540
2602
  stop = true;
2541
2603
  context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2542
2604
  context.emitter.emit("stop", stopAllowance);
2543
2605
  return;
2544
2606
  }
2607
+ if (now - firstSigintAt < 250) return;
2545
2608
  context.logger.warn("[process] second SIGINT: running cleanup then exit");
2546
2609
  await runRegisteredCleanups(context);
2547
2610
  process.exit(2);
@@ -3179,7 +3242,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
3179
3242
  t.timestamp("past_due").defaultTo(null);
3180
3243
  t.text("name").notNullable();
3181
3244
  t.text("opid");
3182
- t.json("params");
3245
+ t.jsonb("params");
3183
3246
  t.text("service_group");
3184
3247
  t.integer("instance_number");
3185
3248
  t.text("service_name");
@@ -3188,7 +3251,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
3188
3251
  t.timestamp("status_changed_at").defaultTo(null);
3189
3252
  t.text("progress");
3190
3253
  t.boolean("success");
3191
- t.json("results");
3254
+ t.jsonb("results");
3192
3255
  t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
3193
3256
  t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
3194
3257
  }
@@ -3203,11 +3266,29 @@ function taskHistoryInsertFromQueueRow(row, overrides) {
3203
3266
  async function ensureTaskTables(context, options = {}) {
3204
3267
  const queueName = options.queueName ?? "tasks";
3205
3268
  const recreate = options.recreate ?? false;
3269
+ const dryRun = options.dryRun ?? false;
3206
3270
  const db = getDb(context);
3271
+ const log = context.logger ?? console;
3207
3272
  const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
3208
3273
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
3209
3274
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
3210
3275
  const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
3276
+ if (dryRun) {
3277
+ const plan = [];
3278
+ if (recreate) {
3279
+ plan.push(`DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`);
3280
+ }
3281
+ if (needsTasks) plan.push(`CREATE TABLE ${tasksTable} (tasks queue)`);
3282
+ if (needsHistory) plan.push(`CREATE TABLE ${historyTable} (history mirror)`);
3283
+ if (needsRegistry) plan.push(`CREATE TABLE ${registryTable} (services registry)`);
3284
+ if (plan.length === 0) {
3285
+ log.info?.(`[tasks-schema] dryRun \u2014 queue "${queueName}" already up to date; no DDL`);
3286
+ } else {
3287
+ log.info?.(`[tasks-schema] dryRun \u2014 would run ${plan.length} statement(s) for queue "${queueName}":`);
3288
+ for (const s of plan) log.info?.(` - ${s}`);
3289
+ }
3290
+ return;
3291
+ }
3211
3292
  if (recreate) {
3212
3293
  await db.schema.dropTableIfExists(historyTable);
3213
3294
  await db.schema.dropTableIfExists(tasksTable);
@@ -5630,9 +5711,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5630
5711
  const dbName = String(context?.params?.get?.("dbName") || "local");
5631
5712
  const tableName = String(context?.params?.get?.("table") || "tasks");
5632
5713
  const fallbackRecoverCommand = [
5633
- "npx",
5634
- "tsx",
5635
- "examples/tasks/recover-task.ts",
5714
+ "node",
5715
+ "examples/tasks/recover-task.js",
5636
5716
  `--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
5637
5717
  `--table='${tableName.replace(/'/g, `'\\''`)}'`,
5638
5718
  `--id='${String(row.id).replace(/'/g, `'\\''`)}'`
@@ -5698,6 +5778,8 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
5698
5778
  const db = getDb3(context);
5699
5779
  let query = db(tasksTable).where({ status: "idle" }).where(function() {
5700
5780
  this.whereNull("service_group").orWhere({ service_group: serviceGroup });
5781
+ }).where(function() {
5782
+ this.whereNull("next_run_at").orWhere("next_run_at", "<=", db.fn.now());
5701
5783
  }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
5702
5784
  if (taskNames && taskNames.length > 0) {
5703
5785
  query = query.whereIn("name", taskNames);