all repos — cmd @ b09b5886d479dc11c1aa1e0c0caabebb8f8e6b0c

Unnamed repository; edit this file 'description' to name the repository.

init
vi did:web:vt3e.cat
Sun, 10 May 2026 01:21:01 +0100
commit

b09b5886d479dc11c1aa1e0c0caabebb8f8e6b0c

7 files changed, 369 insertions(+), 0 deletions(-)

jump to
A .gitignore

@@ -0,0 +1,34 @@

+# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store
A README.md

@@ -0,0 +1,15 @@

+# arg-parser + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.3.13. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
A bun.lock

@@ -0,0 +1,26 @@

+{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "arg-parser", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + } +}
A package.json

@@ -0,0 +1,11 @@

+{ + "name": "arg-parser", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +}
A src/index.ts

@@ -0,0 +1,234 @@

+import type { ArgumentOptions, FlagOptions, Prettify } from "./types"; + +export class Command<TContext extends Record<string, any> = {}> { + public name?: string; + public description?: string; + private flags: Record<string, FlagOptions> = {}; + private args: Record<string, ArgumentOptions<any>> = {}; + private positionals: Array<{ name: string; options: any }> = []; + private subcommands: Record<string, Command<any>> = {}; + private actionFn?: (ctx: TContext) => void | Promise<void>; + + constructor(name?: string) { + this.name = name; + } + + setName(name: string): this { + this.name = name; + return this; + } + + setDescription(desc: string): this { + this.description = desc; + return this; + } + + addFlag<TName extends string>( + name: TName, + options?: FlagOptions, + ): Command<Prettify<TContext & { [K in TName]: boolean }>> { + this.flags[name] = options || {}; + return this; + } + + addStringArgument<TName extends string, TReq extends boolean = false>( + name: TName, + options?: Omit<ArgumentOptions<"string">, "type"> & { required?: TReq }, + ): Command< + Prettify< + TContext & { + [K in TName]: TReq extends true ? string : string | undefined; + } + > + > { + this.args[name] = { ...options, type: "string" }; + return this; + } + + addNumberArgument<TName extends string, TReq extends boolean = false>( + name: TName, + options?: Omit<ArgumentOptions<"number">, "type"> & { required?: TReq }, + ): Command< + Prettify< + TContext & { + [K in TName]: TReq extends true ? number : number | undefined; + } + > + > { + this.args[name] = { ...options, type: "number" }; + return this; + } + + addChoiceArgument< + TName extends string, + const TChoices extends readonly string[], + TReq extends boolean = false, + >( + name: TName, + options: { + choices: TChoices; + required?: TReq; + description?: string; + short?: string; + }, + ): Command< + Prettify< + TContext & { + [K in TName]: TReq extends true + ? TChoices[number] + : TChoices[number] | undefined; + } + > + > { + this.args[name] = { ...options, type: "choice" }; + return this; + } + + addPositional<TName extends string, TReq extends boolean = false>( + name: TName, + options?: { description?: string; required?: TReq }, + ): Command< + Prettify< + TContext & { + [K in TName]: TReq extends true ? string : string | undefined; + } + > + > { + this.positionals = this.positionals || []; + this.positionals.push({ name, options }); + return this; + } + + addSubcommand(command: Command<any>): this { + if (!command.name) throw new Error("Subcommands must have a name"); + + this.subcommands[command.name] = command; + return this; + } + + setAction(fn: (ctx: TContext) => void | Promise<void>): this { + this.actionFn = fn; + return this; + } + + async parse(argv: string[]): Promise<void> { + const ctx: any = {}; + const positionalsProvided: string[] = []; + + // init boolean flags to their defaults, or false. + for (const [key, opts] of Object.entries(this.flags)) { + ctx[key] = opts.default ?? false; + } + + let i = 0; + while (i < argv.length) { + const arg = argv[i] as string; + + // check for subcommands before evaluating positionals/flags + if (!arg.startsWith("-") && this.subcommands[arg]) { + return this.subcommands[arg]?.parse(argv.slice(i + 1)); + } + + const isLong = arg.startsWith("--"); + const isShort = arg.startsWith("-") && !isLong; + + if (isLong) { + let name = arg.slice(2); + let value: string | undefined; + + if (name.includes("=")) { + const splitIdx = name.indexOf("="); + value = name.slice(splitIdx + 1); + name = name.slice(0, splitIdx); + } + + if (this.flags[name]) { + ctx[name] = true; + } else { + const argOpts = this.args[name]; + if (argOpts) { + if ( + value === undefined && + i + 1 < argv.length && + !(argv[i + 1] as string).startsWith("-") + ) { + value = argv[i + 1]; + i++; + } + ctx[name] = this.parseValue(name, value, argOpts); + } else { + throw new Error(`Unknown option: --${name}`); + } + } + } + + if (isShort) { + const short = arg.slice(1); + const flagEntry = Object.entries(this.flags).find( + ([_, o]) => o.short === short, + ); + const argEntry = Object.entries(this.args).find( + ([_, o]) => o.short === short, + ); + + if (flagEntry) { + ctx[flagEntry[0]] = true; + } else if (argEntry) { + const [name, opts] = argEntry; + let value: string | undefined; + if (i + 1 < argv.length && !(argv[i + 1] as string).startsWith("-")) { + value = argv[i + 1]; + i++; + } + ctx[name] = this.parseValue(name, value, opts); + } else { + throw new Error(`Unknown short option: -${short}`); + } + } + + if (!isLong && !isShort) positionalsProvided.push(arg); + + i++; + } + + for (let p = 0; p < this.positionals.length; p++) { + const pos = this.positionals[p]!; + const val = positionalsProvided[p]; + if (val !== undefined) ctx[pos.name] = val; + else if (pos.options?.required) + throw new Error(`Missing required positional argument: <${pos.name}>`); + } + + for (const [name, opts] of Object.entries(this.args)) { + if (opts.required && ctx[name] === undefined) + throw new Error(`Missing required argument: --${name}`); + } + + if (this.actionFn) await this.actionFn(ctx); + } + + private parseValue( + name: string, + value: string | undefined, + opts: ArgumentOptions<any>, + ) { + if (value === undefined) + throw new Error(`Option --${name} requires a value`); + + if (opts.type === "number") { + const num = Number(value); + if (isNaN(num)) + throw new Error(`Option --${name} must be a valid number`); + + return num; + } + if (opts.type === "choice") { + const choices = (opts as any).choices; + if (!choices.includes(value)) + throw new Error( + `Option --${name} must be one of: ${choices.join(", ")}`, + ); + } + return value; + } +}
A src/types.ts

@@ -0,0 +1,16 @@

+export type Prettify<T> = { + [K in keyof T]: T[K]; +} & {}; + +export type FlagOptions = { + description?: string; + short?: string; + default?: boolean; +}; + +export type ArgumentOptions<TType> = { + description?: string; + type: TType; + required?: boolean; + short?: string; +};
A tsconfig.json

@@ -0,0 +1,33 @@

+{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"], + }, + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + }, +}