@vimpak/args 0.1.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.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @vimpak/args
2
+
3
+ CLI argument and configuration loader for TypeScript.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @vimpak/args
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Load config from environment variables (`app__` prefix → dot notation)
14
+ - Load from `etc/env`, `etc/argv`, `etc/config.json`, `etc/secret.json`
15
+ - Command/subcommand support
16
+ - Type-safe config with runtime validation
17
+
18
+ ## Usage
19
+
20
+ ### main()
21
+
22
+ When multiple commands created in one file.
23
+
24
+ ```typescript
25
+ import {main} from "@vimpak/args";
26
+
27
+ interface Config {
28
+ port: number;
29
+ host: string;
30
+ }
31
+
32
+ await main({
33
+ config: {port: 3000, host: "localhost"},
34
+ commands: {
35
+ start: async (config) => {
36
+ console.log(`Starting on ${config.host}:${config.port}`);
37
+ },
38
+ },
39
+ defaultCommand: ["start"],
40
+ });
41
+ ```
42
+
43
+ ### run()
44
+
45
+ When each command is created in different file.
46
+
47
+ ```typescript
48
+ import {run} from "@vimpak/args";
49
+
50
+ interface Config {
51
+ port: number;
52
+ host: string;
53
+ }
54
+
55
+ await run(
56
+ async (config, options) => {
57
+ console.log(`Starting on ${config.host}:${config.port}`);
58
+ },
59
+ {port: 3000, host: "localhost"},
60
+ );
61
+ ```
62
+
63
+ ## Configuration Sources
64
+
65
+ | Source | Format | Example |
66
+ | ----------- | ------------------------ | -------------------------------- |
67
+ | Environment | `app__key__nested=value` | `app__port=3000` → `config.port` |
68
+ | CLI args | `--key value` | `--port 3000` → `config.port` |
69
+ | Config file | JSON | `{"port": 3000}` |
@@ -0,0 +1,19 @@
1
+ export declare function applyOptions<T>(config: T, options: Map<string, string[]>, prefix?: string): void;
2
+ export interface Args {
3
+ cmd: string[];
4
+ options: Map<string, string[]>;
5
+ }
6
+ export declare function loadArgs<T>(config: unknown, defaultCommand?: string[]): Promise<Args>;
7
+ export type Command<T> = (config: T, options: Map<string, string[]>, ...args: string[]) => Promise<void>;
8
+ export interface CommandMap<T> {
9
+ [cmd: string]: undefined | Command<T> | CommandMap<T>;
10
+ }
11
+ export interface Main<T> {
12
+ config: T;
13
+ commands: CommandMap<T>;
14
+ defaultCommand: string[];
15
+ }
16
+ export declare function main<T>({ commands, config, defaultCommand }: Main<T>): Promise<void>;
17
+ export declare function run<T>(command: Command<T>, config: T): Promise<void>;
18
+ export declare function finalizeOptions<T>(config: T, options: Map<string, string[]>): T;
19
+ export declare function doNotUseUnknownOptions(options: Map<string, string[]>): void;
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs/promises";
2
+ function invariant(condition, message) {
3
+ if (!condition) {
4
+ throw new Error(message);
5
+ }
6
+ }
7
+ async function readFile(filename) {
8
+ return fs.readFile(filename, "utf-8");
9
+ }
10
+ async function loadEnv() {
11
+ let content = "";
12
+ try {
13
+ content = await readFile("etc/env");
14
+ }
15
+ catch {
16
+ return;
17
+ }
18
+ const lines = content
19
+ .split("\n")
20
+ .map((item) => item.trim())
21
+ .filter((item) => !item.startsWith("#") && lines.length !== 0);
22
+ for (const line of lines) {
23
+ const parts = line.split("=");
24
+ invariant(parts.length === 2);
25
+ const key = parts[0].trim();
26
+ const value = parts[1].trim();
27
+ if (!process.env[key])
28
+ process.env[key] = value;
29
+ }
30
+ }
31
+ async function readArgv() {
32
+ const argv = process.argv.slice(2);
33
+ let content = "";
34
+ try {
35
+ content = await readFile("etc/argv");
36
+ }
37
+ catch {
38
+ return argv;
39
+ }
40
+ const lines = content
41
+ .split("\n")
42
+ .map((item) => item.trim())
43
+ .filter((item) => !item.startsWith("#") && item.length !== 0);
44
+ for (let i = argv.length; i < lines.length; i++) {
45
+ argv.push(lines[i]);
46
+ }
47
+ return argv;
48
+ }
49
+ async function readOptions() {
50
+ const envPrefix = "app__";
51
+ const optionPredix = "--";
52
+ const configKeyValues = new Map();
53
+ for (const envVar in process.env) {
54
+ if (envVar.startsWith(envPrefix)) {
55
+ const key = envVar.substring(envPrefix.length).replaceAll("__", ".");
56
+ const value = process.env[envVar];
57
+ configKeyValues.set(key, [value]);
58
+ }
59
+ }
60
+ let option = ".";
61
+ const args = await readArgv();
62
+ for (const arg of args) {
63
+ if (arg.startsWith(optionPredix)) {
64
+ option = arg.replace("--", ".");
65
+ }
66
+ else {
67
+ const options = configKeyValues.get(option);
68
+ if (options) {
69
+ options.push(arg);
70
+ }
71
+ else {
72
+ configKeyValues.set(option, [arg]);
73
+ }
74
+ }
75
+ }
76
+ return configKeyValues;
77
+ }
78
+ export function applyOptions(config, options, prefix = "") {
79
+ for (const key in config) {
80
+ const path = `${prefix}.${key}`;
81
+ const configValue = config[key];
82
+ const configType = typeof configValue;
83
+ const values = options.get(path) || [];
84
+ let parsedValue = undefined;
85
+ invariant(configValue !== null);
86
+ if (configType === "object") {
87
+ applyOptions(config[key], options, path);
88
+ }
89
+ else {
90
+ invariant(values.length <= 1);
91
+ const value = values[0];
92
+ if (value === undefined) {
93
+ parsedValue = undefined;
94
+ }
95
+ else if (configType === "number") {
96
+ parsedValue = Number.parseFloat(value);
97
+ }
98
+ else if (configType === "string") {
99
+ parsedValue = value;
100
+ }
101
+ else if (configType === "boolean") {
102
+ if (value === "true") {
103
+ parsedValue = true;
104
+ }
105
+ else if (value === "false") {
106
+ parsedValue = false;
107
+ }
108
+ else {
109
+ throw new Error("Invalid value for boolean");
110
+ }
111
+ }
112
+ }
113
+ if (parsedValue !== undefined) {
114
+ // eslint-disable-next-line
115
+ // @ts-ignore type is checked, but typescript is unable to infer
116
+ config[key] = parsedValue;
117
+ }
118
+ options.delete(path);
119
+ }
120
+ }
121
+ async function applyConfigFile(config, filename) {
122
+ let content;
123
+ try {
124
+ content = await readFile(filename);
125
+ }
126
+ catch {
127
+ return config;
128
+ }
129
+ const values = JSON.parse(content);
130
+ merge(config, values);
131
+ return config;
132
+ }
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ function merge(config, values) {
135
+ invariant(typeof config === typeof values);
136
+ for (const key in config) {
137
+ if (typeof values[key] === "undefined")
138
+ continue;
139
+ invariant(typeof config[key] === typeof values[key]);
140
+ if (values[key] &&
141
+ typeof values[key] === "object" &&
142
+ !Array.isArray(values[key])) {
143
+ merge(config[key], values[key]);
144
+ }
145
+ else {
146
+ config[key] = values[key];
147
+ }
148
+ }
149
+ }
150
+ export async function loadArgs(config, defaultCommand = []) {
151
+ await loadEnv();
152
+ const options = await readOptions();
153
+ const cmd = options.get(".") || defaultCommand;
154
+ options.delete(".");
155
+ const configfiles = options.get(".config");
156
+ if (configfiles) {
157
+ for (const filename of configfiles) {
158
+ await applyConfigFile(config, filename);
159
+ }
160
+ }
161
+ else {
162
+ try {
163
+ await applyConfigFile(config, "etc/config.json");
164
+ }
165
+ catch {
166
+ // pass
167
+ }
168
+ }
169
+ const secretfiles = options.get(".secret");
170
+ options.delete(".secret");
171
+ if (secretfiles) {
172
+ for (const filename of secretfiles) {
173
+ await applyConfigFile(config, filename);
174
+ }
175
+ }
176
+ else {
177
+ try {
178
+ await applyConfigFile(config, "etc/secret.json");
179
+ }
180
+ catch {
181
+ // pass
182
+ }
183
+ }
184
+ applyOptions(config, options);
185
+ return { cmd, options };
186
+ }
187
+ export async function main({ commands, config, defaultCommand }) {
188
+ const { cmd, options } = await loadArgs(config, defaultCommand);
189
+ for (let index = 0; index < cmd.length; index++) {
190
+ const next = commands[cmd[index]];
191
+ if (typeof next === "undefined") {
192
+ throw new Error(`${cmd} is not a valid command`);
193
+ }
194
+ else if (typeof next === "function") {
195
+ await next(config, options, ...cmd);
196
+ }
197
+ else {
198
+ commands = next;
199
+ }
200
+ }
201
+ throw new Error("subcommand is not specfied");
202
+ }
203
+ export async function run(command, config) {
204
+ const { cmd, options } = await loadArgs(config, []);
205
+ await command(config, options, ...cmd);
206
+ doNotUseUnknownOptions(options);
207
+ }
208
+ export function finalizeOptions(config, options) {
209
+ applyOptions(config, options);
210
+ doNotUseUnknownOptions(options);
211
+ return config;
212
+ }
213
+ export function doNotUseUnknownOptions(options) {
214
+ if (options.size === 0)
215
+ return;
216
+ for (const option of options.keys()) {
217
+ console.error(`Unknown option ${option}`);
218
+ }
219
+ throw new Error();
220
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "type": "module",
3
+ "version": "0.1.0",
4
+ "name": "@vimpak/args",
5
+ "collaborators": [
6
+ "Sudesh Yadav <sudeshyadav955@gmail.com>"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://gitlab.com/vimpak/args"
11
+ },
12
+ "files": [
13
+ "./dist"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "prettify": "prettier --write --ignore-unknown ."
24
+ },
25
+ "devDependencies": {
26
+ "@types/bun": "^1.3.11",
27
+ "prettier": "^3.8.1",
28
+ "typescript": "^6.0.2"
29
+ },
30
+ "prettier": {
31
+ "useTabs": true,
32
+ "proseWrap": "always",
33
+ "trailingComma": "all",
34
+ "bracketSpacing": false
35
+ }
36
+ }