├── entrypoint.ts ├── example.ts ├── package.json ├── README.md ├── src ├── Cmd.ts ├── util.ts └── Grub.ts ├── .gitignore ├── tsconfig.json └── bun.lock /entrypoint.ts: -------------------------------------------------------------------------------- 1 | export * from './src/Grub' 2 | export * from './src/Cmd' 3 | export * from './src/util' -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | import { Grub } from "./src/Grub"; 2 | import { Cmd } from "./src/Cmd"; 3 | 4 | export let help = new Cmd('help'); 5 | help.setAsDefault(); 6 | help.setOperation(async () => { 7 | console.log('welcome to grub!') 8 | console.log('the ultimate cli builder!') 9 | }) 10 | 11 | let cli = new Grub(help) 12 | try { 13 | await cli.run(); 14 | } catch (err: any) { 15 | console.error(err.message); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grub", 3 | "version": "0.0.1", 4 | "main": "./entrypoint.ts", 5 | "types": "./entrypoint.ts", 6 | "exports": { 7 | ".": { 8 | "import": "./entrypoint.ts", 9 | "types": "./entrypoint.ts" 10 | } 11 | }, 12 | "files": [ 13 | "src" 14 | ], 15 | "devDependencies": { 16 | "@types/bun": "latest" 17 | }, 18 | "peerDependencies": { 19 | "typescript": "^5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grub 2 | simple cli apps 3 | 4 | ## Installation 5 | ```bash 6 | bun add github:phillip-england/grub 7 | ``` 8 | 9 | ## Usage 10 | ```ts 11 | export let help = new Cmd('help'); 12 | help.setAsDefault(); 13 | help.setOperation(async () => { 14 | console.log('welcome to grub!') 15 | console.log('the ultimate cli builder!') 16 | }) 17 | 18 | let cli = new Grub(help) 19 | try { 20 | await cli.run(); 21 | } catch (err: any) { 22 | console.error(err.message); 23 | } 24 | ``` -------------------------------------------------------------------------------- /src/Cmd.ts: -------------------------------------------------------------------------------- 1 | export class Cmd { 2 | name: string; 3 | isDefault: boolean; 4 | operation: () => Promise; 5 | constructor(name: string) { 6 | this.name = name; 7 | this.isDefault = false; 8 | this.operation = async () => { 9 | throw new Error(`operation not set on cmd named: ${this.name}`); 10 | } 11 | } 12 | setAsDefault() { 13 | this.isDefault = true; 14 | } 15 | setOperation(op: () => Promise) { 16 | this.operation = op; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function getArgByPos(pos: number): string { 2 | let args = Bun.argv; 3 | let selectedArg = args[pos]; 4 | if (selectedArg) { 5 | return selectedArg; 6 | } 7 | return ''; 8 | } 9 | 10 | export function hasFlag(flag: string): boolean { 11 | let args = Bun.argv; 12 | for (let i = 0; i < args.length; i++) { 13 | let arg = args[i]; 14 | if (arg == flag) { 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | 21 | export function argIsNumber(pos: number): boolean { 22 | let args = Bun.argv; 23 | let selectedArg = args[pos]; 24 | if (!selectedArg) { 25 | return false; 26 | } 27 | return !isNaN(Number(selectedArg)) && selectedArg.trim() !== ''; 28 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "Preserve", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | "noImplicitOverride": true, 23 | 24 | // Some stricter flags (disabled by default) 25 | "noUnusedLocals": false, 26 | "noUnusedParameters": false, 27 | "noPropertyAccessFromIndexSignature": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "grub", 6 | "devDependencies": { 7 | "@types/bun": "latest", 8 | }, 9 | "peerDependencies": { 10 | "typescript": "^5", 11 | }, 12 | }, 13 | }, 14 | "packages": { 15 | "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], 16 | 17 | "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], 18 | 19 | "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 20 | 21 | "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], 22 | 23 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 24 | 25 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 26 | 27 | "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Grub.ts: -------------------------------------------------------------------------------- 1 | import { Cmd } from "./Cmd"; 2 | import { getArgByPos } from "./util"; 3 | 4 | export class Grub { 5 | defaultCmd: Cmd; 6 | thirdArg: string; 7 | cmds: Cmd[]; 8 | constructor(...cmds: Cmd[]) { 9 | this.cmds = cmds; 10 | this.defaultCmd = this.locateDefaultCmd() 11 | this.thirdArg = this.loadThirdArg() 12 | this.errOnDuplicateNames(); 13 | } 14 | locateDefaultCmd(): Cmd { 15 | let defaultCmds: Cmd[] = []; 16 | for (let i = 0; i < this.cmds.length; i++) { 17 | let cmd = this.cmds[i] as Cmd; 18 | if (cmd.isDefault) { 19 | defaultCmds.push(cmd); 20 | } 21 | } 22 | if (defaultCmds.length == 0) { 23 | throw new Error(`GRUB ERR: no default cmd located`) 24 | } 25 | if (defaultCmds.length > 1) { 26 | throw new Error(`GRUB ERR: multiple default cmds located`) 27 | } 28 | return defaultCmds[0] as Cmd; 29 | } 30 | loadThirdArg(): string { 31 | let thirdArg = getArgByPos(2); 32 | if (thirdArg == '') { 33 | return 'default'; 34 | } 35 | return thirdArg; 36 | } 37 | async run() { 38 | let cmd = this.locateCmdToRun() 39 | await cmd.operation(); 40 | } 41 | locateCmdToRun(): Cmd { 42 | if (this.thirdArg == 'default') { 43 | return this.defaultCmd; 44 | } else { 45 | for (let i = 0; i < this.cmds.length; i++) { 46 | let cmd = this.cmds[i] as Cmd; 47 | if (this.thirdArg == cmd.name) { 48 | return cmd; 49 | } 50 | } 51 | } 52 | throw new Error(`no cmd named ${this.thirdArg} located`); 53 | } 54 | errOnDuplicateNames() { 55 | let foundNames: string[] = []; 56 | for (let i = 0; i < this.cmds.length; i++) { 57 | let cmd = this.cmds[i] as Cmd; 58 | if (foundNames.includes(cmd.name)) { 59 | throw new Error(`you have two cmds named ${cmd.name}`) 60 | } 61 | foundNames.push(cmd.name); 62 | } 63 | } 64 | } --------------------------------------------------------------------------------