CLI Configuration Management Patterns
How to build flexible, user-friendly configuration systems for command-line tools using layered config files, environment variables, and runtime flags.
CLI Configuration Management Patterns
Every CLI tool that grows beyond a single command needs configuration. Users want defaults they can override, project-level settings that differ from global ones, and the ability to pass flags without editing files. Getting configuration right means your tool feels natural. Getting it wrong means users fight the tool instead of using it.
I have built dozens of CLI tools and the configuration layer is always where complexity sneaks in. This guide covers the patterns that work in production, from simple rc files to full layered configuration systems.
Prerequisites
- Node.js installed (v14+)
- Basic familiarity with building CLI tools
- Understanding of JSON and YAML formats
- Experience with environment variables
The Configuration Hierarchy
Most mature CLI tools follow a layered configuration model. Each layer overrides the previous one, giving users control at every level.
The standard precedence order, from lowest to highest priority:
- Built-in defaults — hardcoded in your tool
- System-wide config —
/etc/mytool/configor equivalent - User-level config —
~/.mytoolrcor~/.config/mytool/config - Project-level config —
.mytoolrcin the project directory - Environment variables —
MYTOOL_*prefixed vars - Command-line flags —
--option value
var path = require("path");
var fs = require("fs");
var os = require("os");
function loadConfig(cliFlags) {
// Layer 1: Built-in defaults
var config = {
output: "json",
verbose: false,
timeout: 30000,
retries: 3,
color: true,
logLevel: "info"
};
// Layer 2: User-level config (~/.mytoolrc)
var userConfig = loadConfigFile(
path.join(os.homedir(), ".mytoolrc")
);
if (userConfig) {
Object.assign(config, userConfig);
}
// Layer 3: Project-level config (./.mytoolrc)
var projectConfig = loadConfigFile(
path.join(process.cwd(), ".mytoolrc")
);
if (projectConfig) {
Object.assign(config, projectConfig);
}
// Layer 4: Environment variables
var envConfig = loadEnvConfig("MYTOOL_");
Object.assign(config, envConfig);
// Layer 5: CLI flags (highest priority)
Object.assign(config, cliFlags);
return config;
}
function loadConfigFile(filePath) {
try {
var content = fs.readFileSync(filePath, "utf8");
return JSON.parse(content);
} catch (err) {
if (err.code === "ENOENT") {
return null;
}
console.error("Error reading config file " + filePath + ": " + err.message);
return null;
}
}
function loadEnvConfig(prefix) {
var config = {};
var keys = Object.keys(process.env);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key.indexOf(prefix) === 0) {
var configKey = key
.slice(prefix.length)
.toLowerCase()
.replace(/_([a-z])/g, function(match, letter) {
return letter.toUpperCase();
});
config[configKey] = coerceValue(process.env[key]);
}
}
return config;
}
function coerceValue(value) {
if (value === "true") return true;
if (value === "false") return false;
if (value === "null") return null;
var num = Number(value);
if (!isNaN(num) && value.trim() !== "") return num;
return value;
}
RC File Patterns
The "rc" file convention dates back to Unix. Tools like .bashrc, .npmrc, and .eslintrc established the pattern that most developers expect.
Simple JSON RC Files
The simplest approach uses JSON files in well-known locations:
var path = require("path");
var fs = require("fs");
var os = require("os");
var RC_FILENAME = ".deploytoolrc";
function findRcFiles() {
var files = [];
// User home directory
var homeRc = path.join(os.homedir(), RC_FILENAME);
if (fs.existsSync(homeRc)) {
files.push(homeRc);
}
// Walk up from cwd to find project-level rc files
var dir = process.cwd();
var root = path.parse(dir).root;
var projectFiles = [];
while (dir !== root) {
var rcPath = path.join(dir, RC_FILENAME);
if (fs.existsSync(rcPath)) {
projectFiles.push(rcPath);
}
dir = path.dirname(dir);
}
// Reverse so parent directories are applied first
projectFiles.reverse();
files = files.concat(projectFiles);
return files;
}
function mergeRcFiles() {
var config = {};
var files = findRcFiles();
for (var i = 0; i < files.length; i++) {
try {
var content = fs.readFileSync(files[i], "utf8");
var parsed = JSON.parse(content);
deepMerge(config, parsed);
console.log("Loaded config from " + files[i]);
} catch (err) {
console.error("Invalid config in " + files[i] + ": " + err.message);
}
}
return config;
}
function deepMerge(target, source) {
var keys = Object.keys(source);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (
source[key] &&
typeof source[key] === "object" &&
!Array.isArray(source[key]) &&
target[key] &&
typeof target[key] === "object"
) {
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Example .deploytoolrc file:
{
"provider": "aws",
"region": "us-west-2",
"deploy": {
"strategy": "rolling",
"maxSurge": 2,
"healthCheckPath": "/health"
},
"notifications": {
"slack": true,
"channel": "#deployments"
}
}
Using cosmiconfig for Flexible Formats
The cosmiconfig package handles the heavy lifting of searching for configuration files in multiple formats. ESLint, Prettier, and Babel all use this approach.
var cosmiconfigModule = require("cosmiconfig");
var cosmiconfig = cosmiconfigModule.cosmiconfig;
var explorer = cosmiconfig("mytool", {
searchPlaces: [
"package.json",
".mytoolrc",
".mytoolrc.json",
".mytoolrc.yaml",
".mytoolrc.yml",
".mytoolrc.js",
".mytoolrc.cjs",
"mytool.config.js",
"mytool.config.cjs"
]
});
function loadConfig() {
var result = explorer.searchSync();
if (result) {
console.log("Config loaded from: " + result.filepath);
return result.config;
}
console.log("No config file found, using defaults");
return {};
}
// Load from a specific path
function loadConfigFrom(filePath) {
var result = explorer.loadSync(filePath);
return result ? result.config : {};
}
This lets users choose their preferred format. In package.json:
{
"name": "my-project",
"mytool": {
"output": "table",
"verbose": true
}
}
Or in mytool.config.js:
module.exports = {
output: "table",
verbose: process.env.NODE_ENV === "development"
};
The JavaScript config format is powerful because users can include computed values, conditionals, and even async operations.
XDG Base Directory Specification
Modern CLI tools should respect the XDG Base Directory specification on Linux and macOS. Instead of cluttering the home directory with dotfiles, config goes in ~/.config/ and data in ~/.local/share/.
var path = require("path");
var os = require("os");
var APP_NAME = "mytool";
function getConfigDir() {
var platform = process.platform;
if (platform === "win32") {
return path.join(
process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"),
APP_NAME
);
}
if (platform === "darwin") {
// macOS: prefer XDG if set, otherwise ~/Library/Preferences
if (process.env.XDG_CONFIG_HOME) {
return path.join(process.env.XDG_CONFIG_HOME, APP_NAME);
}
return path.join(os.homedir(), "Library", "Preferences", APP_NAME);
}
// Linux and others: XDG
var xdgConfig = process.env.XDG_CONFIG_HOME ||
path.join(os.homedir(), ".config");
return path.join(xdgConfig, APP_NAME);
}
function getDataDir() {
var platform = process.platform;
if (platform === "win32") {
return path.join(
process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
APP_NAME
);
}
if (platform === "darwin") {
if (process.env.XDG_DATA_HOME) {
return path.join(process.env.XDG_DATA_HOME, APP_NAME);
}
return path.join(os.homedir(), "Library", "Application Support", APP_NAME);
}
var xdgData = process.env.XDG_DATA_HOME ||
path.join(os.homedir(), ".local", "share");
return path.join(xdgData, APP_NAME);
}
function getCacheDir() {
var platform = process.platform;
if (platform === "win32") {
return path.join(
process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
APP_NAME,
"Cache"
);
}
if (platform === "darwin") {
return path.join(os.homedir(), "Library", "Caches", APP_NAME);
}
var xdgCache = process.env.XDG_CACHE_HOME ||
path.join(os.homedir(), ".cache");
return path.join(xdgCache, APP_NAME);
}
// Usage
var configDir = getConfigDir();
var dataDir = getDataDir();
var cacheDir = getCacheDir();
console.log("Config: " + configDir);
console.log("Data: " + dataDir);
console.log("Cache: " + cacheDir);
Output on Linux:
Config: /home/user/.config/mytool
Data: /home/user/.local/share/mytool
Cache: /home/user/.cache/mytool
Output on macOS:
Config: /Users/user/Library/Preferences/mytool
Data: /Users/user/Library/Application Support/mytool
Cache: /Users/user/Library/Caches/mytool
Output on Windows:
Config: C:\Users\user\AppData\Roaming\mytool
Data: C:\Users\user\AppData\Local\mytool
Cache: C:\Users\user\AppData\Local\mytool\Cache
Environment Variable Patterns
Environment variables are the standard way to configure tools in CI/CD pipelines and containers where config files are inconvenient.
Prefix-Based Mapping
Use a consistent prefix to namespace your tool's environment variables:
var ENV_PREFIX = "DEPLOY_";
var ENV_MAP = {
"DEPLOY_PROVIDER": { key: "provider", type: "string" },
"DEPLOY_REGION": { key: "region", type: "string" },
"DEPLOY_DRY_RUN": { key: "dryRun", type: "boolean" },
"DEPLOY_MAX_RETRIES": { key: "maxRetries", type: "number" },
"DEPLOY_TAGS": { key: "tags", type: "array" },
"DEPLOY_STRATEGY": { key: "deploy.strategy", type: "string" },
"DEPLOY_MAX_SURGE": { key: "deploy.maxSurge", type: "number" }
};
function loadEnvConfig() {
var config = {};
var envKeys = Object.keys(ENV_MAP);
for (var i = 0; i < envKeys.length; i++) {
var envKey = envKeys[i];
var value = process.env[envKey];
if (value === undefined) continue;
var mapping = ENV_MAP[envKey];
var parsed = parseEnvValue(value, mapping.type);
setNestedKey(config, mapping.key, parsed);
}
return config;
}
function parseEnvValue(value, type) {
switch (type) {
case "boolean":
return value === "true" || value === "1" || value === "yes";
case "number":
var num = Number(value);
if (isNaN(num)) {
throw new Error("Expected number, got: " + value);
}
return num;
case "array":
return value.split(",").map(function(item) {
return item.trim();
});
default:
return value;
}
}
function setNestedKey(obj, keyPath, value) {
var parts = keyPath.split(".");
var current = obj;
for (var i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
Usage in CI:
export DEPLOY_PROVIDER=aws
export DEPLOY_REGION=us-east-1
export DEPLOY_DRY_RUN=false
export DEPLOY_MAX_RETRIES=5
export DEPLOY_TAGS=production,v2.1.0,hotfix
mytool deploy
Dotenv Integration
Support .env files for local development without polluting the real environment:
var fs = require("fs");
var path = require("path");
function loadDotenv(dir) {
var envFile = path.join(dir || process.cwd(), ".env");
try {
var content = fs.readFileSync(envFile, "utf8");
var lines = content.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
// Skip empty lines and comments
if (!line || line.charAt(0) === "#") continue;
var eqIndex = line.indexOf("=");
if (eqIndex === -1) continue;
var key = line.substring(0, eqIndex).trim();
var value = line.substring(eqIndex + 1).trim();
// Remove surrounding quotes
if (
(value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') ||
(value.charAt(0) === "'" && value.charAt(value.length - 1) === "'")
) {
value = value.substring(1, value.length - 1);
}
// Only set if not already defined (real env takes precedence)
if (process.env[key] === undefined) {
process.env[key] = value;
}
}
} catch (err) {
if (err.code !== "ENOENT") {
console.error("Error reading .env: " + err.message);
}
}
}
Config Init and Config Show Commands
Good CLI tools let users create and inspect their configuration interactively.
var fs = require("fs");
var path = require("path");
var os = require("os");
var readline = require("readline");
var DEFAULT_CONFIG = {
provider: "aws",
region: "us-east-1",
output: "json",
deploy: {
strategy: "rolling",
maxSurge: 1
}
};
function configInit(options) {
var configPath = options.global
? path.join(os.homedir(), ".mytoolrc")
: path.join(process.cwd(), ".mytoolrc");
if (fs.existsSync(configPath) && !options.force) {
console.error("Config file already exists: " + configPath);
console.error("Use --force to overwrite");
process.exit(1);
}
if (options.interactive) {
interactiveInit(configPath);
} else {
var content = JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n";
fs.writeFileSync(configPath, content);
console.log("Created config file: " + configPath);
}
}
function interactiveInit(configPath) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
var config = {};
rl.question("Provider (aws/gcp/azure) [aws]: ", function(provider) {
config.provider = provider || "aws";
rl.question("Region [us-east-1]: ", function(region) {
config.region = region || "us-east-1";
rl.question("Output format (json/table/yaml) [json]: ", function(output) {
config.output = output || "json";
rl.question("Deploy strategy (rolling/blue-green/canary) [rolling]: ", function(strategy) {
config.deploy = {
strategy: strategy || "rolling",
maxSurge: 1
};
var content = JSON.stringify(config, null, 2) + "\n";
fs.writeFileSync(configPath, content);
console.log("\nCreated config file: " + configPath);
rl.close();
});
});
});
});
}
function configShow(options) {
var config = loadConfig({});
if (options.key) {
var value = getNestedKey(config, options.key);
if (value === undefined) {
console.error("Key not found: " + options.key);
process.exit(1);
}
console.log(typeof value === "object" ? JSON.stringify(value, null, 2) : value);
return;
}
if (options.sources) {
showConfigSources();
return;
}
console.log(JSON.stringify(config, null, 2));
}
function showConfigSources() {
var sources = [
{ name: "defaults", path: "built-in" },
{ name: "global", path: path.join(os.homedir(), ".mytoolrc") },
{ name: "project", path: path.join(process.cwd(), ".mytoolrc") },
{ name: "env", path: "MYTOOL_* environment variables" }
];
console.log("Configuration sources (in priority order):\n");
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
var exists = source.name === "defaults" || source.name === "env"
? true
: fs.existsSync(source.path);
var status = exists ? "[active]" : "[not found]";
console.log(" " + (i + 1) + ". " + source.name + " " + status);
console.log(" " + source.path);
}
}
function getNestedKey(obj, keyPath) {
var parts = keyPath.split(".");
var current = obj;
for (var i = 0; i < parts.length; i++) {
if (current === undefined || current === null) return undefined;
current = current[parts[i]];
}
return current;
}
Usage:
# Create global config interactively
mytool config init --global --interactive
# Create project config
mytool config init
# Show merged config
mytool config show
# Show a specific key
mytool config show --key deploy.strategy
# Show where config is coming from
mytool config show --sources
Output of config show --sources:
Configuration sources (in priority order):
1. defaults [active]
built-in
2. global [active]
/home/user/.mytoolrc
3. project [active]
/home/user/projects/webapp/.mytoolrc
4. env [active]
MYTOOL_* environment variables
Config Validation with JSON Schema
Never trust config file contents. Validate everything at load time to give users clear error messages:
function validateConfig(config, schema) {
var errors = [];
validateObject(config, schema, "", errors);
if (errors.length > 0) {
console.error("Configuration errors:\n");
for (var i = 0; i < errors.length; i++) {
console.error(" - " + errors[i]);
}
process.exit(1);
}
return config;
}
function validateObject(obj, schema, prefix, errors) {
var schemaKeys = Object.keys(schema);
for (var i = 0; i < schemaKeys.length; i++) {
var key = schemaKeys[i];
var rule = schema[key];
var fullKey = prefix ? prefix + "." + key : key;
var value = obj[key];
// Check required fields
if (rule.required && (value === undefined || value === null)) {
errors.push(fullKey + " is required");
continue;
}
if (value === undefined) continue;
// Type checking
if (rule.type === "object" && rule.properties) {
if (typeof value !== "object" || Array.isArray(value)) {
errors.push(fullKey + " must be an object");
} else {
validateObject(value, rule.properties, fullKey, errors);
}
continue;
}
if (rule.type && typeof value !== rule.type) {
errors.push(fullKey + " must be type " + rule.type + ", got " + typeof value);
continue;
}
// Enum validation
if (rule.enum && rule.enum.indexOf(value) === -1) {
errors.push(fullKey + " must be one of: " + rule.enum.join(", "));
}
// Range validation
if (rule.min !== undefined && value < rule.min) {
errors.push(fullKey + " must be >= " + rule.min);
}
if (rule.max !== undefined && value > rule.max) {
errors.push(fullKey + " must be <= " + rule.max);
}
// Pattern validation
if (rule.pattern && !new RegExp(rule.pattern).test(value)) {
errors.push(fullKey + " must match pattern: " + rule.pattern);
}
}
// Warn about unknown keys
var objKeys = Object.keys(obj);
for (var j = 0; j < objKeys.length; j++) {
if (!schema[objKeys[j]]) {
errors.push((prefix || "root") + " has unknown key: " + objKeys[j]);
}
}
}
// Schema definition
var CONFIG_SCHEMA = {
provider: {
type: "string",
required: true,
enum: ["aws", "gcp", "azure"]
},
region: {
type: "string",
required: true,
pattern: "^[a-z]{2}-[a-z]+-\\d+$"
},
output: {
type: "string",
enum: ["json", "table", "yaml", "csv"]
},
timeout: {
type: "number",
min: 1000,
max: 300000
},
deploy: {
type: "object",
properties: {
strategy: {
type: "string",
enum: ["rolling", "blue-green", "canary"]
},
maxSurge: {
type: "number",
min: 1,
max: 10
}
}
}
};
// Validate loaded config
var config = loadConfig({});
validateConfig(config, CONFIG_SCHEMA);
Bad config produces clear errors:
Configuration errors:
- provider must be one of: aws, gcp, azure
- region must match pattern: ^[a-z]{2}-[a-z]+-\d+$
- timeout must be >= 1000
- deploy.strategy must be one of: rolling, blue-green, canary
- root has unknown key: notReal
Complete Working Example: Configurable Deploy CLI
Here is a complete CLI tool with a full-featured configuration system using all the patterns above:
#!/usr/bin/env node
var fs = require("fs");
var path = require("path");
var os = require("os");
// ---- Configuration System ----
var APP_NAME = "deploycli";
var RC_FILENAME = "." + APP_NAME + "rc";
var DEFAULTS = {
provider: "aws",
region: "us-east-1",
output: "table",
verbose: false,
color: true,
timeout: 30000,
deploy: {
strategy: "rolling",
maxSurge: 1,
healthCheckPath: "/health",
healthCheckInterval: 10
},
notifications: {
enabled: false,
slack: null,
email: null
}
};
function getConfigDir() {
if (process.platform === "win32") {
return path.join(process.env.APPDATA || os.homedir(), APP_NAME);
}
var xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
return path.join(xdg, APP_NAME);
}
function loadJsonFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return null;
throw new Error("Invalid JSON in " + filePath + ": " + err.message);
}
}
function deepMerge(target, source) {
if (!source) return target;
var result = JSON.parse(JSON.stringify(target));
var keys = Object.keys(source);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (
source[key] && typeof source[key] === "object" &&
!Array.isArray(source[key]) &&
result[key] && typeof result[key] === "object"
) {
result[key] = deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
function loadEnvVars() {
var prefix = APP_NAME.toUpperCase() + "_";
var config = {};
var keys = Object.keys(process.env);
for (var i = 0; i < keys.length; i++) {
if (keys[i].indexOf(prefix) !== 0) continue;
var configKey = keys[i].slice(prefix.length).toLowerCase();
// Handle nested keys: DEPLOYCLI_DEPLOY_STRATEGY -> deploy.strategy
var parts = configKey.split("_");
var obj = config;
for (var j = 0; j < parts.length - 1; j++) {
if (!obj[parts[j]]) obj[parts[j]] = {};
obj = obj[parts[j]];
}
var val = process.env[keys[i]];
if (val === "true") val = true;
else if (val === "false") val = false;
else if (!isNaN(Number(val)) && val.trim() !== "") val = Number(val);
obj[parts[parts.length - 1]] = val;
}
return config;
}
function resolveConfig(cliFlags) {
var config = JSON.parse(JSON.stringify(DEFAULTS));
var sources = ["defaults"];
// XDG / global config
var globalPath = path.join(getConfigDir(), "config.json");
var globalConfig = loadJsonFile(globalPath);
if (globalConfig) {
config = deepMerge(config, globalConfig);
sources.push("global:" + globalPath);
}
// User home rc
var homeRc = path.join(os.homedir(), RC_FILENAME);
var homeConfig = loadJsonFile(homeRc);
if (homeConfig) {
config = deepMerge(config, homeConfig);
sources.push("user:" + homeRc);
}
// Project rc (walk up)
var dir = process.cwd();
var root = path.parse(dir).root;
while (dir !== root) {
var projectRc = path.join(dir, RC_FILENAME);
var projectConfig = loadJsonFile(projectRc);
if (projectConfig) {
config = deepMerge(config, projectConfig);
sources.push("project:" + projectRc);
break;
}
dir = path.dirname(dir);
}
// Environment variables
var envConfig = loadEnvVars();
if (Object.keys(envConfig).length > 0) {
config = deepMerge(config, envConfig);
sources.push("env");
}
// CLI flags
if (cliFlags && Object.keys(cliFlags).length > 0) {
config = deepMerge(config, cliFlags);
sources.push("cli");
}
config._sources = sources;
return config;
}
// ---- CLI Argument Parser ----
function parseArgs(argv) {
var args = argv.slice(2);
var command = null;
var flags = {};
var positional = [];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg.indexOf("--") === 0) {
var key = arg.slice(2);
var eqIdx = key.indexOf("=");
if (eqIdx !== -1) {
flags[key.substring(0, eqIdx)] = key.substring(eqIdx + 1);
} else if (i + 1 < args.length && args[i + 1].indexOf("-") !== 0) {
flags[key] = args[++i];
} else {
flags[key] = true;
}
} else if (arg.indexOf("-") === 0) {
flags[arg.slice(1)] = true;
} else if (!command) {
command = arg;
} else {
positional.push(arg);
}
}
return { command: command, flags: flags, positional: positional };
}
// ---- Commands ----
function cmdConfigInit(config, flags) {
var targetPath = flags.global
? path.join(os.homedir(), RC_FILENAME)
: path.join(process.cwd(), RC_FILENAME);
if (fs.existsSync(targetPath) && !flags.force) {
console.error("Config exists: " + targetPath + " (use --force to overwrite)");
process.exit(1);
}
var template = {
provider: "aws",
region: "us-east-1",
deploy: { strategy: "rolling" }
};
var dir = path.dirname(targetPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n");
console.log("Created: " + targetPath);
}
function cmdConfigShow(config, flags) {
if (flags.sources) {
console.log("Active configuration sources:");
var srcs = config._sources || [];
for (var i = 0; i < srcs.length; i++) {
console.log(" " + (i + 1) + ". " + srcs[i]);
}
return;
}
var display = JSON.parse(JSON.stringify(config));
delete display._sources;
if (flags.key) {
var parts = flags.key.split(".");
var val = display;
for (var j = 0; j < parts.length; j++) {
val = val ? val[parts[j]] : undefined;
}
console.log(val !== undefined ? (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)) : "undefined");
return;
}
console.log(JSON.stringify(display, null, 2));
}
function cmdDeploy(config, flags, positional) {
var target = positional[0] || "production";
console.log("Deploying to " + target + "...");
console.log(" Provider: " + config.provider);
console.log(" Region: " + config.region);
console.log(" Strategy: " + config.deploy.strategy);
console.log(" Health: " + config.deploy.healthCheckPath);
if (config.verbose) {
console.log("\nFull config:");
var display = JSON.parse(JSON.stringify(config));
delete display._sources;
console.log(JSON.stringify(display, null, 2));
}
}
// ---- Main ----
function main() {
var parsed = parseArgs(process.argv);
var configFlags = {};
// Map CLI flags to config
if (parsed.flags.provider) configFlags.provider = parsed.flags.provider;
if (parsed.flags.region) configFlags.region = parsed.flags.region;
if (parsed.flags.output) configFlags.output = parsed.flags.output;
if (parsed.flags.v || parsed.flags.verbose) configFlags.verbose = true;
if (parsed.flags["no-color"]) configFlags.color = false;
var config = resolveConfig(configFlags);
switch (parsed.command) {
case "config":
var subCommand = parsed.positional[0];
if (subCommand === "init") {
cmdConfigInit(config, parsed.flags);
} else {
cmdConfigShow(config, parsed.flags);
}
break;
case "deploy":
cmdDeploy(config, parsed.flags, parsed.positional);
break;
default:
console.log("Usage: deploycli <command> [options]");
console.log("");
console.log("Commands:");
console.log(" deploy [target] Deploy application");
console.log(" config init Create config file");
console.log(" config show Show merged configuration");
console.log("");
console.log("Options:");
console.log(" --provider <name> Cloud provider (aws, gcp, azure)");
console.log(" --region <region> Deploy region");
console.log(" --output <format> Output format (json, table)");
console.log(" --verbose, -v Verbose output");
console.log(" --no-color Disable color output");
break;
}
}
main();
Run it:
# Show config sources
$ deploycli config show --sources
Active configuration sources:
1. defaults
2. user:/home/user/.deployclirc
3. project:/home/user/webapp/.deployclirc
4. env
# Show specific config key
$ deploycli config show --key deploy.strategy
rolling
# Deploy with CLI override
$ deploycli deploy staging --provider gcp --region us-central1 --verbose
Deploying to staging...
Provider: gcp
Region: us-central1
Strategy: rolling
Health: /health
Full config:
{
"provider": "gcp",
"region": "us-central1",
...
}
Common Issues and Troubleshooting
Config file not found despite existing
The tool might be looking in the wrong directory. Most rc file walkers start from process.cwd(), not the script's location:
Error: Cannot find .mytoolrc
Fix: Run the tool from your project root, or use --config /path/to/config to specify explicitly.
Environment variable overrides not working
Environment variable names are case-sensitive on Linux but not on Windows. Always use uppercase for env vars:
# Wrong on Linux (if tool expects uppercase)
export deploycli_region=us-west-2
# Correct
export DEPLOYCLI_REGION=us-west-2
JSON parse errors in config files
Trailing commas and comments are the usual culprits. JSON does not allow either:
SyntaxError: Unexpected token } in JSON at position 142
Fix: Use a JSON linter or switch to YAML format. If you want comments, support .js config files.
Conflicting config from multiple sources
When debugging, always provide a way to show where each value came from:
$ mytool config show --sources
If values seem wrong, check each source in priority order. Environment variables are a common culprit, especially in CI where previous jobs may have exported vars.
File permission errors on config directory
On Linux, the XDG config directory might not exist or have wrong permissions:
Error: EACCES: permission denied, open '/home/user/.config/mytool/config.json'
Fix: Create the directory with proper permissions:
mkdir -p ~/.config/mytool
chmod 700 ~/.config/mytool
Best Practices
- Always provide sensible defaults. The tool should work with zero configuration. Config files are for customization, not requirements.
- Follow platform conventions. Use XDG on Linux,
~/Libraryon macOS,%APPDATA%on Windows. Users expect their tools to behave consistently with the platform. - Validate early and fail with clear messages. Check config at startup and report every error at once, not one at a time.
- Support
--configflag to load from arbitrary paths. CI pipelines and containers need this flexibility. - Document every config key. A
config initcommand with comments or aconfig docssubcommand saves users from digging through source code. - Never store secrets in config files by default. Credentials should come from environment variables, keychains, or dedicated credential stores. If your config file contains a token, warn the user.
- Make config inspection easy. A
config showcommand that displays the final merged config with source attribution is invaluable for debugging. - Use deep merge, not shallow merge. Shallow
Object.assignoverwrites entire nested objects when users only want to change one key inside them.