@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.
@@ -1758,46 +1758,87 @@ 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()) | "stop" (print after init, then
1765
+ * exit the process — also via context.showUsedParamsIfNeeded()).
1766
+ */
1767
+ _showUsedParamsMode = false;
1768
+ /** Guard so the dump prints at most once (top OR end, never both). */
1769
+ _usedParamsPrinted = false;
1763
1770
  constructor(context, options = {}) {
1764
1771
  this.context = context;
1765
1772
  this.args = context.args;
1766
1773
  if (Object.keys(options).length > 0) {
1767
1774
  this.configure(options);
1768
1775
  }
1769
- this._showUsedParams = this.get("showUsedParams", "boolean default false");
1776
+ this._resolveShowUsedParams();
1770
1777
  if (context && typeof context.registerCleanup === "function") {
1771
1778
  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
- }
1779
+ if (!ctx.params.getShowUsedParamsMode()) return;
1780
+ ctx.params.printUsedParams(ctx.logger);
1795
1781
  });
1796
1782
  }
1797
1783
  }
1798
- /** Whether --showUsedParams was requested (resolved in constructor). */
1784
+ /**
1785
+ * Resolve the --showUsedParams mode. The flag is intentionally dual-typed:
1786
+ * (absent) / --no-showUsedParams / =false -> false (off)
1787
+ * --showUsedParams / =true -> "end" (print at exit)
1788
+ * --showUsedParams=top -> "top" (print after init)
1789
+ * --showUsedParams=stop -> "stop" (print after init, then exit)
1790
+ * Read raw (uncoerced) from args so the string "top"/"stop" isn't forced to
1791
+ * a boolean, then track it under the "script" module for the dump itself.
1792
+ */
1793
+ _resolveShowUsedParams() {
1794
+ const raw = this.args.get("showUsedParams");
1795
+ const source = this.args.getSource?.("showUsedParams") ?? "default";
1796
+ let mode = false;
1797
+ if (raw === void 0 || raw === null) {
1798
+ mode = false;
1799
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "top") {
1800
+ mode = "top";
1801
+ } else if (typeof raw === "string" && raw.trim().toLowerCase() === "stop") {
1802
+ mode = "stop";
1803
+ } else {
1804
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : raw;
1805
+ const falsey = s === false || s === "false" || s === "0" || s === "no" || s === "off";
1806
+ mode = falsey ? false : "end";
1807
+ }
1808
+ this._showUsedParamsMode = mode;
1809
+ this.trackParam("showUsedParams", "string", mode, raw === void 0 ? "default" : source, "script");
1810
+ return mode;
1811
+ }
1812
+ /** Whether --showUsedParams was requested in any mode (truthy = on). */
1799
1813
  getShowUsedParams() {
1800
- return this._showUsedParams;
1814
+ return this._showUsedParamsMode !== false;
1815
+ }
1816
+ /** Resolved mode: false | "end" | "top" | "stop". */
1817
+ getShowUsedParamsMode() {
1818
+ return this._showUsedParamsMode;
1819
+ }
1820
+ /**
1821
+ * Print the module-grouped list of figured params (the --showUsedParams
1822
+ * dump). Idempotent: only the first call prints, so callers can invoke it
1823
+ * at the top (long-running services) without double-printing at exit.
1824
+ */
1825
+ printUsedParams(logger) {
1826
+ if (this._usedParamsPrinted) return;
1827
+ const byModule = this.getFiguredByModule();
1828
+ const modules = Object.keys(byModule).sort();
1829
+ if (modules.length === 0) return;
1830
+ this._usedParamsPrinted = true;
1831
+ logger = logger ?? this.context?.logger ?? console;
1832
+ const hasHighlight = typeof logger.highlight === "function";
1833
+ logger.debug("[Params]: list of used params:");
1834
+ for (const mod of modules) {
1835
+ logger.debug(` [${mod}]`);
1836
+ for (const [key, entry] of Object.entries(byModule[mod])) {
1837
+ const valueStr = JSON.stringify(entry.value);
1838
+ const display = hasHighlight && entry.source !== "default" ? logger.highlight(valueStr) : valueStr;
1839
+ logger.debug(` ${key}: ${display} (${entry.source})`);
1840
+ }
1841
+ }
1801
1842
  }
1802
1843
  /**
1803
1844
  * Configure parameters
@@ -2482,7 +2523,25 @@ function setup(opts = {}) {
2482
2523
  emitter: partialContext.emitter,
2483
2524
  isStop: partialContext.isStop,
2484
2525
  cleanupFunctions: partialContext.cleanupFunctions,
2485
- registerCleanup: partialContext.registerCleanup
2526
+ registerCleanup: partialContext.registerCleanup,
2527
+ // For long-running scripts (servers): with --showUsedParams=top, print
2528
+ // the used-params list now (after the script has initialized all its
2529
+ // own components), instead of at exit. No-op for the default mode,
2530
+ // which prints at exit via the cleanup registered by Params.
2531
+ //
2532
+ // --showUsedParams=stop behaves like "top" but then exits immediately —
2533
+ // a quick "show me the figured params and quit" that skips the flow's
2534
+ // actual work. Like --stopAfter=init, this is a hard exit(0) (registered
2535
+ // cleanups are skipped); call it once components/params are resolved.
2536
+ showUsedParamsIfNeeded: () => {
2537
+ const mode = params.getShowUsedParamsMode?.();
2538
+ if (mode !== "top" && mode !== "stop") return;
2539
+ params.printUsedParams(logger);
2540
+ if (mode === "stop") {
2541
+ logger.debug?.("[showUsedParams=stop] params printed \u2014 exiting");
2542
+ process.exit(0);
2543
+ }
2544
+ }
2486
2545
  };
2487
2546
  logger.debug("[setup] completed successfully");
2488
2547
  return context;
@@ -2549,15 +2608,19 @@ async function init(flow2, opts = {}) {
2549
2608
  process.exit(0);
2550
2609
  }
2551
2610
  let sigintCount = 0;
2611
+ let firstSigintAt = 0;
2552
2612
  process.on("SIGINT", async () => {
2553
2613
  if (!context) return;
2554
- sigintCount += 1;
2555
- if (sigintCount === 1) {
2614
+ const now = Date.now();
2615
+ if (sigintCount === 0) {
2616
+ sigintCount = 1;
2617
+ firstSigintAt = now;
2556
2618
  stop = true;
2557
2619
  context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
2558
2620
  context.emitter.emit("stop", stopAllowance);
2559
2621
  return;
2560
2622
  }
2623
+ if (now - firstSigintAt < 250) return;
2561
2624
  context.logger.warn("[process] second SIGINT: running cleanup then exit");
2562
2625
  await runRegisteredCleanups(context);
2563
2626
  process.exit(2);
@@ -3195,7 +3258,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
3195
3258
  t.timestamp("past_due").defaultTo(null);
3196
3259
  t.text("name").notNullable();
3197
3260
  t.text("opid");
3198
- t.json("params");
3261
+ t.jsonb("params");
3199
3262
  t.text("service_group");
3200
3263
  t.integer("instance_number");
3201
3264
  t.text("service_name");
@@ -3204,7 +3267,7 @@ function defineTasksTable(t, db, tableNameForIndex) {
3204
3267
  t.timestamp("status_changed_at").defaultTo(null);
3205
3268
  t.text("progress");
3206
3269
  t.boolean("success");
3207
- t.json("results");
3270
+ t.jsonb("results");
3208
3271
  t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
3209
3272
  t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
3210
3273
  }
@@ -3219,11 +3282,29 @@ function taskHistoryInsertFromQueueRow(row, overrides) {
3219
3282
  async function ensureTaskTables(context, options = {}) {
3220
3283
  const queueName = options.queueName ?? "tasks";
3221
3284
  const recreate = options.recreate ?? false;
3285
+ const dryRun = options.dryRun ?? false;
3222
3286
  const db = getDb(context);
3287
+ const log = context.logger ?? console;
3223
3288
  const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
3224
3289
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
3225
3290
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
3226
3291
  const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
3292
+ if (dryRun) {
3293
+ const plan = [];
3294
+ if (recreate) {
3295
+ plan.push(`DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`);
3296
+ }
3297
+ if (needsTasks) plan.push(`CREATE TABLE ${tasksTable} (tasks queue)`);
3298
+ if (needsHistory) plan.push(`CREATE TABLE ${historyTable} (history mirror)`);
3299
+ if (needsRegistry) plan.push(`CREATE TABLE ${registryTable} (services registry)`);
3300
+ if (plan.length === 0) {
3301
+ log.info?.(`[tasks-schema] dryRun \u2014 queue "${queueName}" already up to date; no DDL`);
3302
+ } else {
3303
+ log.info?.(`[tasks-schema] dryRun \u2014 would run ${plan.length} statement(s) for queue "${queueName}":`);
3304
+ for (const s of plan) log.info?.(` - ${s}`);
3305
+ }
3306
+ return;
3307
+ }
3227
3308
  if (recreate) {
3228
3309
  await db.schema.dropTableIfExists(historyTable);
3229
3310
  await db.schema.dropTableIfExists(tasksTable);
@@ -5646,9 +5727,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
5646
5727
  const dbName = String(context?.params?.get?.("dbName") || "local");
5647
5728
  const tableName = String(context?.params?.get?.("table") || "tasks");
5648
5729
  const fallbackRecoverCommand = [
5649
- "npx",
5650
- "tsx",
5651
- "examples/tasks/recover-task.ts",
5730
+ "node",
5731
+ "examples/tasks/recover-task.js",
5652
5732
  `--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
5653
5733
  `--table='${tableName.replace(/'/g, `'\\''`)}'`,
5654
5734
  `--id='${String(row.id).replace(/'/g, `'\\''`)}'`
@@ -5714,6 +5794,8 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
5714
5794
  const db = getDb3(context);
5715
5795
  let query = db(tasksTable).where({ status: "idle" }).where(function() {
5716
5796
  this.whereNull("service_group").orWhere({ service_group: serviceGroup });
5797
+ }).where(function() {
5798
+ this.whereNull("next_run_at").orWhere("next_run_at", "<=", db.fn.now());
5717
5799
  }).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);
5718
5800
  if (taskNames && taskNames.length > 0) {
5719
5801
  query = query.whereIn("name", taskNames);