|
| 1 | +/** |
| 2 | + * @fileoverview `ibmi config` commands for inspecting CLI configuration. |
| 3 | + * Shows merged configuration with per-setting origin tracking. |
| 4 | + * @module cli/commands/config |
| 5 | + */ |
| 6 | + |
| 7 | +import { Command } from "commander"; |
| 8 | +import { |
| 9 | + loadConfig, |
| 10 | + loadConfigLayers, |
| 11 | + getUserConfigPath, |
| 12 | + type ConfigLayer, |
| 13 | +} from "../config/index.js"; |
| 14 | +import { renderOutput, renderError } from "../formatters/output.js"; |
| 15 | +import { ExitCode } from "../utils/exit-codes.js"; |
| 16 | +import { getFormat } from "../utils/command-helpers.js"; |
| 17 | + |
| 18 | +/** |
| 19 | + * Determine which config layer a top-level key (`default` or `format`) came from. |
| 20 | + * Project layer takes precedence if it defines the key. |
| 21 | + */ |
| 22 | +function findOrigin(layers: ConfigLayer[], key: "default" | "format"): string { |
| 23 | + // Check project first (higher precedence) |
| 24 | + for (const layer of [...layers].reverse()) { |
| 25 | + if (layer.config && layer.config[key] !== undefined) { |
| 26 | + return layer.scope; |
| 27 | + } |
| 28 | + } |
| 29 | + return "unknown"; |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Determine which config layer a system definition came from. |
| 34 | + * Project layer takes precedence for same-named systems. |
| 35 | + */ |
| 36 | +function findSystemOrigin(layers: ConfigLayer[], name: string): string { |
| 37 | + for (const layer of [...layers].reverse()) { |
| 38 | + if (layer.config && name in layer.config.systems) { |
| 39 | + return layer.scope; |
| 40 | + } |
| 41 | + } |
| 42 | + return "unknown"; |
| 43 | +} |
| 44 | + |
| 45 | +/** Environment variables checked for config overrides. */ |
| 46 | +const CONFIG_ENV_VARS = ["IBMI_SYSTEM", "DB2i_HOST", "DB2i_USER", "DB2i_PASS"]; |
| 47 | + |
| 48 | +/** Env vars whose values must be masked in output. */ |
| 49 | +const SENSITIVE_ENV_VARS = new Set(["DB2i_PASS"]); |
| 50 | + |
| 51 | +/** Return active environment variable overrides that affect config resolution. */ |
| 52 | +function getActiveEnvOverrides(): { name: string; value: string }[] { |
| 53 | + const overrides: { name: string; value: string }[] = []; |
| 54 | + |
| 55 | + for (const name of CONFIG_ENV_VARS) { |
| 56 | + const value = process.env[name]; |
| 57 | + if (value) { |
| 58 | + overrides.push({ name, value: SENSITIVE_ENV_VARS.has(name) ? "****" : value }); |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + return overrides; |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Register `ibmi config` subcommands. |
| 67 | + */ |
| 68 | +export function registerConfigCommand(program: Command): void { |
| 69 | + const config = program |
| 70 | + .command("config") |
| 71 | + .description("Inspect CLI configuration") |
| 72 | + .action(() => { |
| 73 | + config.outputHelp(); |
| 74 | + }); |
| 75 | + |
| 76 | + config |
| 77 | + .command("show") |
| 78 | + .description("Show active configuration with file origins") |
| 79 | + .action((_opts, cmd: Command) => { |
| 80 | + const format = getFormat(cmd); |
| 81 | + try { |
| 82 | + const layers = loadConfigLayers(); |
| 83 | + const merged = loadConfig(); |
| 84 | + const data: Record<string, unknown>[] = []; |
| 85 | + |
| 86 | + // File layer status |
| 87 | + const userPath = getUserConfigPath(); |
| 88 | + const userLayer = layers.find((l) => l.scope === "user"); |
| 89 | + data.push({ |
| 90 | + PROPERTY: "[user]", |
| 91 | + VALUE: userLayer?.exists ? "loaded" : "not found", |
| 92 | + SOURCE: userPath, |
| 93 | + }); |
| 94 | + |
| 95 | + const projectLayer = layers.find((l) => l.scope === "project"); |
| 96 | + if (projectLayer) { |
| 97 | + data.push({ |
| 98 | + PROPERTY: "[project]", |
| 99 | + VALUE: "loaded", |
| 100 | + SOURCE: projectLayer.path, |
| 101 | + }); |
| 102 | + } |
| 103 | + |
| 104 | + // Top-level settings |
| 105 | + if (merged.default) { |
| 106 | + data.push({ |
| 107 | + PROPERTY: "default", |
| 108 | + VALUE: merged.default, |
| 109 | + SOURCE: findOrigin(layers, "default"), |
| 110 | + }); |
| 111 | + } |
| 112 | + |
| 113 | + if (merged.format) { |
| 114 | + data.push({ |
| 115 | + PROPERTY: "format", |
| 116 | + VALUE: merged.format, |
| 117 | + SOURCE: findOrigin(layers, "format"), |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + // Systems |
| 122 | + for (const [name, sys] of Object.entries(merged.systems)) { |
| 123 | + data.push({ |
| 124 | + PROPERTY: `systems.${name}`, |
| 125 | + VALUE: `${sys.host}:${sys.port} (${sys.user})`, |
| 126 | + SOURCE: findSystemOrigin(layers, name), |
| 127 | + }); |
| 128 | + } |
| 129 | + |
| 130 | + // Active env overrides |
| 131 | + for (const override of getActiveEnvOverrides()) { |
| 132 | + data.push({ |
| 133 | + PROPERTY: override.name, |
| 134 | + VALUE: override.value, |
| 135 | + SOURCE: "environment", |
| 136 | + }); |
| 137 | + } |
| 138 | + |
| 139 | + renderOutput(data, format, { rowCount: data.length }); |
| 140 | + } catch (err) { |
| 141 | + renderError( |
| 142 | + err instanceof Error ? err : new Error(String(err)), |
| 143 | + format, |
| 144 | + ); |
| 145 | + process.exitCode = ExitCode.GENERAL; |
| 146 | + } |
| 147 | + }); |
| 148 | +} |
0 commit comments