easycli-config 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avijit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @easycli/config
2
+
3
+ Zero-config configuration loader for EasyCLI apps.
4
+
5
+ ## Features
6
+
7
+ - **Zero Config**: Works out of the box.
8
+ - **Hierarchical Loading**: Loads from home directory, project directory, and environment variables.
9
+ - **Multiple Formats**: Supports JSON, JS, and TS config files.
10
+ - **Type Safe**: Fully typed with TypeScript.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pnpm add @easycli/config
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { loadConfig } from "@easycli/config";
22
+
23
+ interface MyConfig {
24
+ port: number;
25
+ dbUrl: string;
26
+ }
27
+
28
+ // Loads and merges config from all supported sources
29
+ const config = await loadConfig<MyConfig>();
30
+
31
+ console.log(config);
32
+ ```
33
+
34
+ ## Configuration Search Order
35
+
36
+ Configuration is merged in the following order (last one wins):
37
+
38
+ 1. **Home Directory**: `~/.easyclirc.json`
39
+ 2. **Project Directory** (searched in order):
40
+ - `.easyclirc.json`
41
+ - `.easyclirc`
42
+ - `easycli.config.json`
43
+ - `easycli.config.js`
44
+ - `easycli.config.ts`
45
+ 3. **Environment Variables**: Variables starting with `EASYCLI_`
46
+ - `EASYCLI_PORT=3000` -> `{ port: 3000 }`
47
+ - `EASYCLI_DB_URL=postgres://...` -> `{ db: { url: "postgres://..." } }`
48
+
49
+ ## API
50
+
51
+ ### `loadConfig<T>(options?: ConfigLoaderOptions): Promise<T>`
52
+
53
+ Loads the configuration.
54
+
55
+ - **options.cwd**: Directory to search for config files (default: `process.cwd()`)
56
+ - **options.name**: Config name namespace (default: "easycli")
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Options for the configuration loader.
3
+ */
4
+ interface ConfigLoaderOptions {
5
+ /**
6
+ * The current working directory to search for configuration files.
7
+ * Defaults to `process.cwd()`.
8
+ */
9
+ cwd?: string;
10
+ /**
11
+ * Optional name to use for configuration files (defaults to "easycli").
12
+ * Not currently used in logic but reserved for future customization.
13
+ */
14
+ name?: string;
15
+ }
16
+ /**
17
+ * Loads configuration from multiple sources, merging them in the following order:
18
+ * 1. User home directory config (`~/.easyclirc.json`)
19
+ * 2. Project config files (in order of precedence):
20
+ * - `.easyclirc.json`
21
+ * - `.easyclirc`
22
+ * - `easycli.config.json`
23
+ * - `easycli.config.js`
24
+ * - `easycli.config.ts`
25
+ * 3. Environment variables (prefixed with `EASYCLI_`)
26
+ *
27
+ * @template T - The expected shape of the configuration object.
28
+ * @param options - Configuration options.
29
+ * @returns A promise that resolves to the merged configuration object.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * interface MyConfig {
34
+ * port: number;
35
+ * debug: boolean;
36
+ * }
37
+ *
38
+ * const config = await loadConfig<MyConfig>();
39
+ * console.log(config.port);
40
+ * ```
41
+ */
42
+ declare function loadConfig<T = Record<string, unknown>>(options?: ConfigLoaderOptions): Promise<T>;
43
+
44
+ export { type ConfigLoaderOptions, loadConfig };
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ // src/loader.ts
2
+ import { readFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { pathToFileURL } from "url";
6
+ var CONFIG_FILES = [
7
+ ".easyclirc.json",
8
+ ".easyclirc",
9
+ "easycli.config.json",
10
+ "easycli.config.js",
11
+ "easycli.config.ts"
12
+ ];
13
+ async function loadConfig(options = {}) {
14
+ const cwd = options.cwd ?? process.cwd();
15
+ const configs = [];
16
+ const homeConfig = await loadJsonConfig(join(homedir(), ".easyclirc.json"));
17
+ if (homeConfig) {
18
+ configs.push(homeConfig);
19
+ }
20
+ for (const filename of CONFIG_FILES) {
21
+ const filepath = join(cwd, filename);
22
+ if (filename.endsWith(".json") || filename === ".easyclirc") {
23
+ const config = await loadJsonConfig(filepath);
24
+ if (config) {
25
+ configs.push(config);
26
+ }
27
+ } else if (filename.endsWith(".js") || filename.endsWith(".ts")) {
28
+ const config = await loadModuleConfig(filepath);
29
+ if (config) {
30
+ configs.push(config);
31
+ }
32
+ }
33
+ }
34
+ const envConfig = loadEnvConfig();
35
+ if (Object.keys(envConfig).length > 0) {
36
+ configs.push(envConfig);
37
+ }
38
+ return mergeConfigs(configs);
39
+ }
40
+ async function loadJsonConfig(filepath) {
41
+ try {
42
+ const content = await readFile(filepath, "utf-8");
43
+ return JSON.parse(content);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function loadModuleConfig(filepath) {
49
+ try {
50
+ const url = pathToFileURL(filepath).href;
51
+ const mod = await import(url);
52
+ return mod.default ?? null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function loadEnvConfig() {
58
+ const config = {};
59
+ const prefix = "EASYCLI_";
60
+ for (const [key, value] of Object.entries(process.env)) {
61
+ if (key.startsWith(prefix) && value !== void 0) {
62
+ const configKey = key.slice(prefix.length).toLowerCase().replace(/_/g, ".");
63
+ setNestedValue(config, configKey, parseEnvValue(value));
64
+ }
65
+ }
66
+ return config;
67
+ }
68
+ function parseEnvValue(value) {
69
+ if (value === "true") return true;
70
+ if (value === "false") return false;
71
+ const num = Number(value);
72
+ if (!Number.isNaN(num)) return num;
73
+ return value;
74
+ }
75
+ function setNestedValue(obj, path, value) {
76
+ const keys = path.split(".");
77
+ let current = obj;
78
+ for (let i = 0; i < keys.length - 1; i++) {
79
+ const key = keys[i];
80
+ if (key) {
81
+ if (!(key in current)) {
82
+ current[key] = {};
83
+ }
84
+ current = current[key];
85
+ }
86
+ }
87
+ const lastKey = keys[keys.length - 1];
88
+ if (lastKey) {
89
+ current[lastKey] = value;
90
+ }
91
+ }
92
+ function mergeConfigs(configs) {
93
+ const result = {};
94
+ for (const config of configs) {
95
+ deepMerge(result, config);
96
+ }
97
+ return result;
98
+ }
99
+ function deepMerge(target, source) {
100
+ for (const [key, value] of Object.entries(source)) {
101
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && typeof target[key] === "object" && target[key] !== null && !Array.isArray(target[key])) {
102
+ deepMerge(
103
+ target[key],
104
+ value
105
+ );
106
+ } else {
107
+ target[key] = value;
108
+ }
109
+ }
110
+ }
111
+ export {
112
+ loadConfig
113
+ };
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loader.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\r\nimport { join } from \"node:path\";\r\nimport { homedir } from \"node:os\";\r\nimport { pathToFileURL } from \"node:url\";\r\n\r\n/**\r\n * Options for the configuration loader.\r\n */\r\nexport interface ConfigLoaderOptions {\r\n /**\r\n * The current working directory to search for configuration files.\r\n * Defaults to `process.cwd()`.\r\n */\r\n cwd?: string;\r\n /**\r\n * Optional name to use for configuration files (defaults to \"easycli\").\r\n * Not currently used in logic but reserved for future customization.\r\n */\r\n name?: string;\r\n}\r\n\r\nconst CONFIG_FILES = [\r\n \".easyclirc.json\",\r\n \".easyclirc\",\r\n \"easycli.config.json\",\r\n \"easycli.config.js\",\r\n \"easycli.config.ts\"\r\n];\r\n\r\n/**\r\n * Loads configuration from multiple sources, merging them in the following order:\r\n * 1. User home directory config (`~/.easyclirc.json`)\r\n * 2. Project config files (in order of precedence):\r\n * - `.easyclirc.json`\r\n * - `.easyclirc`\r\n * - `easycli.config.json`\r\n * - `easycli.config.js`\r\n * - `easycli.config.ts`\r\n * 3. Environment variables (prefixed with `EASYCLI_`)\r\n *\r\n * @template T - The expected shape of the configuration object.\r\n * @param options - Configuration options.\r\n * @returns A promise that resolves to the merged configuration object.\r\n *\r\n * @example\r\n * ```ts\r\n * interface MyConfig {\r\n * port: number;\r\n * debug: boolean;\r\n * }\r\n *\r\n * const config = await loadConfig<MyConfig>();\r\n * console.log(config.port);\r\n * ```\r\n */\r\nexport async function loadConfig<T = Record<string, unknown>>(\r\n options: ConfigLoaderOptions = {}\r\n): Promise<T> {\r\n const cwd = options.cwd ?? process.cwd();\r\n const configs: Record<string, unknown>[] = [];\r\n\r\n const homeConfig = await loadJsonConfig(join(homedir(), \".easyclirc.json\"));\r\n if (homeConfig) {\r\n configs.push(homeConfig);\r\n }\r\n\r\n for (const filename of CONFIG_FILES) {\r\n const filepath = join(cwd, filename);\r\n if (filename.endsWith(\".json\") || filename === \".easyclirc\") {\r\n const config = await loadJsonConfig(filepath);\r\n if (config) {\r\n configs.push(config);\r\n }\r\n } else if (filename.endsWith(\".js\") || filename.endsWith(\".ts\")) {\r\n const config = await loadModuleConfig(filepath);\r\n if (config) {\r\n configs.push(config);\r\n }\r\n }\r\n }\r\n\r\n const envConfig = loadEnvConfig();\r\n if (Object.keys(envConfig).length > 0) {\r\n configs.push(envConfig);\r\n }\r\n\r\n return mergeConfigs(configs) as T;\r\n}\r\n\r\nasync function loadJsonConfig(\r\n filepath: string\r\n): Promise<Record<string, unknown> | null> {\r\n try {\r\n const content = await readFile(filepath, \"utf-8\");\r\n return JSON.parse(content) as Record<string, unknown>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nasync function loadModuleConfig(\r\n filepath: string\r\n): Promise<Record<string, unknown> | null> {\r\n try {\r\n const url = pathToFileURL(filepath).href;\r\n const mod = (await import(url)) as { default?: Record<string, unknown> };\r\n return mod.default ?? null;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction loadEnvConfig(): Record<string, unknown> {\r\n const config: Record<string, unknown> = {};\r\n const prefix = \"EASYCLI_\";\r\n\r\n for (const [key, value] of Object.entries(process.env)) {\r\n if (key.startsWith(prefix) && value !== undefined) {\r\n const configKey = key\r\n .slice(prefix.length)\r\n .toLowerCase()\r\n .replace(/_/g, \".\");\r\n setNestedValue(config, configKey, parseEnvValue(value));\r\n }\r\n }\r\n\r\n return config;\r\n}\r\n\r\nfunction parseEnvValue(value: string): unknown {\r\n if (value === \"true\") return true;\r\n if (value === \"false\") return false;\r\n const num = Number(value);\r\n if (!Number.isNaN(num)) return num;\r\n return value;\r\n}\r\n\r\nfunction setNestedValue(\r\n obj: Record<string, unknown>,\r\n path: string,\r\n value: unknown\r\n): void {\r\n const keys = path.split(\".\");\r\n let current = obj;\r\n\r\n for (let i = 0; i < keys.length - 1; i++) {\r\n const key = keys[i];\r\n if (key) {\r\n if (!(key in current)) {\r\n current[key] = {};\r\n }\r\n current = current[key] as Record<string, unknown>;\r\n }\r\n }\r\n\r\n const lastKey = keys[keys.length - 1];\r\n if (lastKey) {\r\n current[lastKey] = value;\r\n }\r\n}\r\n\r\nfunction mergeConfigs(configs: Record<string, unknown>[]): Record<string, unknown> {\r\n const result: Record<string, unknown> = {};\r\n\r\n for (const config of configs) {\r\n deepMerge(result, config);\r\n }\r\n\r\n return result;\r\n}\r\n\r\nfunction deepMerge(\r\n target: Record<string, unknown>,\r\n source: Record<string, unknown>\r\n): void {\r\n for (const [key, value] of Object.entries(source)) {\r\n if (\r\n value !== null &&\r\n typeof value === \"object\" &&\r\n !Array.isArray(value) &&\r\n typeof target[key] === \"object\" &&\r\n target[key] !== null &&\r\n !Array.isArray(target[key])\r\n ) {\r\n deepMerge(\r\n target[key] as Record<string, unknown>,\r\n value as Record<string, unknown>\r\n );\r\n } else {\r\n target[key] = value;\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "easycli-config",
3
+ "version": "0.0.1",
4
+ "description": "Configuration loader for EasyCLI",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "devDependencies": {
19
+ "tsup": "^8.0.1",
20
+ "typescript": "^5.3.3",
21
+ "rimraf": "^5.0.5"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "clean": "rimraf dist",
27
+ "typecheck": "tsc --noEmit"
28
+ }
29
+ }