├── packages ├── docs │ ├── .prettierignore │ ├── content │ │ ├── 1.Usage │ │ │ ├── _dir.yml │ │ │ ├── 3.Utils.md │ │ │ ├── 1.Commands.md │ │ │ ├── 4.Configurations.md │ │ │ ├── 5.UsageGenerator.md │ │ │ ├── 0.index.md │ │ │ └── 2.Options.md │ │ ├── 0.intro │ │ │ ├── _dir.yml │ │ │ └── 0.index.md │ │ └── index.md │ ├── tsconfig.json │ ├── nuxt.config.ts │ ├── public │ │ ├── icon.png │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── preview.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ ├── logo-light.svg │ │ └── logo-dark.svg │ ├── .gitignore │ ├── tokens.config.ts │ ├── package.json │ └── app.config.ts ├── mustard-cli │ ├── .gitignore │ ├── .npmignore │ ├── source │ │ ├── Exports │ │ │ ├── Validator.ts │ │ │ ├── ComanndLine.ts │ │ │ ├── Decorators.ts │ │ │ └── index.ts │ │ ├── Typings │ │ │ ├── Controller.struct.ts │ │ │ ├── Factory.struct.ts │ │ │ ├── DIService.struct.ts │ │ │ ├── Utils.struct.ts │ │ │ ├── Temp.ts │ │ │ ├── Command.struct.ts │ │ │ ├── Option.struct.ts │ │ │ ├── Configuration.struct.ts │ │ │ ├── Context.struct.ts │ │ │ └── Shared.struct.ts │ │ ├── __tests__ │ │ │ ├── Fixtures │ │ │ │ ├── TestHelper.ts │ │ │ │ └── UsageFixtures.ts │ │ │ ├── Integrations │ │ │ │ ├── UsageGenerator │ │ │ │ │ ├── NoRootAndCommonCommandsProvided.usage.ts │ │ │ │ │ ├── NoRootAndCommonCommandsProvidedAndDisableUsageInfo.usage.ts │ │ │ │ │ ├── RootCommandOnly.usage.ts │ │ │ │ │ ├── NoRootCommandButAtLeastOneCommandProvided.usage.ts │ │ │ │ │ ├── NoRootCommandButMultipleCommandsRegistered.usage.ts │ │ │ │ │ ├── RootCommandAndCommonCommandsProvided.usage.ts │ │ │ │ │ └── RootAndNestedCommandsProvided.usage.ts │ │ │ │ ├── BuiltInCommands │ │ │ │ │ ├── Usage1.ts │ │ │ │ │ ├── Usage2.ts │ │ │ │ │ ├── Usage3.ts │ │ │ │ │ └── BuiltInCommands.spec.ts │ │ │ │ ├── Root │ │ │ │ │ ├── Usage.ts │ │ │ │ │ └── Root.spec.ts │ │ │ │ ├── RestrictValue │ │ │ │ │ ├── Usage.ts │ │ │ │ │ └── RestrictValue.spec.ts │ │ │ │ ├── MultiRootCommands │ │ │ │ │ ├── Usage.ts │ │ │ │ │ └── MultiRootCommands.spec.ts │ │ │ │ ├── NonCompleteParse │ │ │ │ │ ├── Usage.ts │ │ │ │ │ └── NonCompleteParse.spec.ts │ │ │ │ └── Common │ │ │ │ │ └── Usage.ts │ │ │ ├── UtilsProvider.spec.ts │ │ │ ├── Registry.spec.ts │ │ │ ├── MustardFactory.spec.ts │ │ │ ├── Errors.spec.ts │ │ │ ├── Validator.spec.ts │ │ │ └── BuiltInCommands.spec.ts │ │ ├── Validators │ │ │ ├── index.ts │ │ │ ├── EnumValidators.ts │ │ │ ├── Factory.ts │ │ │ ├── Typings.ts │ │ │ └── PrimitiveValidators.ts │ │ ├── Components │ │ │ ├── ConflictChecker.ts │ │ │ ├── Constants.ts │ │ │ ├── MustardFactory.ts │ │ │ ├── Registry.ts │ │ │ ├── Utils.ts │ │ │ └── MustardUtilsProvider.ts │ │ ├── Errors │ │ │ ├── NullishFactoryOptionError.ts │ │ │ ├── NoRootHandlerError.ts │ │ │ ├── CommandNotFoundError.ts │ │ │ ├── MultiRootCommandError.ts │ │ │ ├── UnknownOptionsError.ts │ │ │ └── ValidationError.ts │ │ ├── Experimentals │ │ │ └── MustardApp.ts │ │ ├── Decorators │ │ │ ├── DIService.ts │ │ │ ├── BuiltIn.ts │ │ │ ├── Controller.ts │ │ │ └── Input.ts │ │ └── Commands │ │ │ ├── BuiltInCommands.ts │ │ │ └── CommandLine.ts │ ├── .vscode │ │ └── settings.json │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── README.md │ ├── CHANGELOG.md │ └── package.json ├── .DS_Store ├── create-mustard-app │ ├── template-simple │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── index.js │ │ ├── tsconfig.json │ │ ├── src │ │ │ └── index.mts │ │ └── package.json │ ├── index.js │ ├── template-advanced │ │ ├── .vscode │ │ │ └── settings.json │ │ ├── index.js │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── src │ │ │ └── index.mts │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ ├── CHANGELOG.md │ └── source │ │ └── index.mts └── sample │ ├── tsconfig.json │ ├── samples │ ├── Nested.ts │ ├── WithCustomProviders.ts │ └── Common.ts │ ├── package.json │ ├── CHANGELOG.md │ └── index.mts ├── .prettierignore ├── .DS_Store ├── experimentals ├── MudtardContainer │ ├── source │ │ ├── index.ts │ │ └── Impls.ts │ ├── package.json │ └── tsconfig.json └── PackageStarter │ ├── package.json │ └── tsconfig.json ├── logo.png ├── sample.png ├── .vscode └── settings.json ├── .github ├── pull_request_template.md └── workflows │ └── workflow.yml ├── pnpm-workspace.yaml ├── .npmrc ├── .changeset ├── config.json └── README.md ├── package.json ├── LICENSE ├── .gitignore └── README.md /packages/docs/.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/docs/**/*.md -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Usage 2 | -------------------------------------------------------------------------------- /packages/docs/content/0.intro/_dir.yml: -------------------------------------------------------------------------------- 1 | title: Introduction 2 | -------------------------------------------------------------------------------- /packages/mustard-cli/.gitignore: -------------------------------------------------------------------------------- 1 | _samples 2 | advanced 3 | .TODO -------------------------------------------------------------------------------- /packages/mustard-cli/.npmignore: -------------------------------------------------------------------------------- 1 | source 2 | coverage 3 | samples -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/.DS_Store -------------------------------------------------------------------------------- /experimentals/MudtardContainer/source/index.ts: -------------------------------------------------------------------------------- 1 | console.log("first"); 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/logo.png -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/sample.png -------------------------------------------------------------------------------- /packages/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/.DS_Store -------------------------------------------------------------------------------- /packages/mustard-cli/source/Exports/Validator.ts: -------------------------------------------------------------------------------- 1 | export { Validator } from "../Validators"; 2 | -------------------------------------------------------------------------------- /packages/docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus' 3 | }) 4 | -------------------------------------------------------------------------------- /packages/docs/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/icon.png -------------------------------------------------------------------------------- /packages/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/logo.png -------------------------------------------------------------------------------- /packages/mustard-cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/favicon.ico -------------------------------------------------------------------------------- /packages/docs/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/preview.png -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Controller.struct.ts: -------------------------------------------------------------------------------- 1 | export type RestrictValueSet = any[] | Record; 2 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-simple/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/create-mustard-app/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --experimental-specifier-resolution=node 2 | 3 | import "./dist/index.mjs"; 4 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-advanced/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | - [ ] Bugfix 4 | - [ ] Docs typo 5 | - [ ] Feature / Enhancement 6 | 7 | ## Proposed Changes 8 | -------------------------------------------------------------------------------- /packages/docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinbuduLab/Mustard/HEAD/packages/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/create-mustard-app/template-simple/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --experimental-specifier-resolution=node 2 | 3 | import "./dist/index.mjs"; 4 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-advanced/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --experimental-specifier-resolution=node 2 | 3 | import "./dist/index.mjs"; 4 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Fixtures/TestHelper.ts: -------------------------------------------------------------------------------- 1 | export class TestHelper { 2 | public static IntegrationExecutor = "ts-node-esm --transpile-only"; 3 | } 4 | -------------------------------------------------------------------------------- /packages/docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme, palette } from 'pinceau' 2 | 3 | export default defineTheme({ 4 | colors: { 5 | primary: palette('orange') 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Factory.struct.ts: -------------------------------------------------------------------------------- 1 | export abstract class MustardApp { 2 | abstract onStart?(): void; 3 | abstract onError?(error: unknown): void; 4 | abstract onComplete?(): void; 5 | } 6 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Validators/index.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFactory } from "./Factory"; 2 | 3 | // control from validateOptions 4 | // defaultRequired: true 5 | export const Validator = new ValidatorFactory(); 6 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Exports/ComanndLine.ts: -------------------------------------------------------------------------------- 1 | export { CLI } from "../Commands/CommandLine"; 2 | 3 | export type { MustardApp } from "../Typings/Factory.struct"; 4 | export type { CommandStruct } from "../Typings/Command.struct"; 5 | -------------------------------------------------------------------------------- /experimentals/MudtardContainer/source/Impls.ts: -------------------------------------------------------------------------------- 1 | export class Initializers { 2 | public static CommonInitializer() {} 3 | } 4 | 5 | export class Decorators { 6 | public static Provide() {} 7 | 8 | public static Inject() {} 9 | } 10 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/ConflictChecker.ts: -------------------------------------------------------------------------------- 1 | import { MustardRegistry } from "./Registry"; 2 | 3 | export class ConflictChecker { 4 | public static beforeRegisterCommand(registryKey: string) {} 5 | 6 | public static beforeRegisterCommandAlias() {} 7 | } 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/** 3 | - experimentals/** 4 | - "!**/test/**" 5 | - "!**/template-**" 6 | link-workspace-packages: false 7 | prefer-workspace-packages: false 8 | shared-workspace-lockfile: true 9 | save-workspace-protocol: true 10 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/DIService.struct.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeFactory, Constructable } from "./Shared.struct"; 2 | 3 | export type Provider = 4 | | { 5 | identifier: unknown; 6 | value: MaybeFactory; 7 | // value: MaybeFactory | MaybeAsyncFactory; 8 | } 9 | | Constructable; 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | hoist=true 2 | # shamefully-hoist=false 3 | shamefully-hoist=true 4 | strict-peer-dependencies=false 5 | store-dir=~/.pnpm-store 6 | modules-dir=node_modules 7 | virtual-store-dir=node_modules/.pnpm 8 | lockfile=true 9 | prefer-frozen-lockfile=true 10 | registry=https://registry.npmjs.org/ 11 | auto-install-peers=false 12 | strict-peer-dependencies=false 13 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/NullishFactoryOptionError.ts: -------------------------------------------------------------------------------- 1 | export class NullishFactoryOptionError extends Error { 2 | public name = "NullishFactoryOptionError"; 3 | 4 | constructor() { 5 | super(); 6 | } 7 | 8 | get message(): string { 9 | return `Mustard factory option not initialized, use @App to initialize entry class`; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["mustard-docs", "mustard-container"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/NoRootAndCommonCommandsProvided.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { App } from "../../../Exports/Decorators"; 3 | 4 | @App({ 5 | name: "mm", 6 | commands: [], 7 | configurations: {}, 8 | }) 9 | class Project {} 10 | 11 | MustardFactory.init(Project).start(); 12 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/NoRootHandlerError.ts: -------------------------------------------------------------------------------- 1 | export class NoRootHandlerError extends Error { 2 | public name = "NoRootHandlerError"; 3 | 4 | constructor() { 5 | super(); 6 | } 7 | 8 | get message(): string { 9 | return `No root handler found, please provide command decorated with '@RootCommand' or enable option enableUsage for usage info generation.`; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/NoRootAndCommonCommandsProvidedAndDisableUsageInfo.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { App } from "../../../Exports/Decorators"; 3 | 4 | @App({ 5 | name: "mm", 6 | commands: [], 7 | configurations: { 8 | enableUsage: false, 9 | }, 10 | }) 11 | class Project {} 12 | 13 | MustardFactory.init(Project).start(); 14 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Experimentals/MustardApp.ts: -------------------------------------------------------------------------------- 1 | export class MustardApp { 2 | registerOption() {} 3 | 4 | registerComnmand(factory: CommandFactory) {} 5 | } 6 | 7 | export class CommandRegistry { 8 | registerOption() {} 9 | } 10 | 11 | type CommandFactory = (command: any) => void; 12 | 13 | const app = new MustardApp(); 14 | 15 | app.registerComnmand((command) => { 16 | command.setup(); 17 | command.configure(); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview" 10 | }, 11 | "devDependencies": { 12 | "@nuxt-themes/docus": "^1.4.7", 13 | "nuxt": "^3.0.0", 14 | "typescript": "^5.3.3" 15 | }, 16 | "engines": { 17 | "node": ">=18" 18 | } 19 | } -------------------------------------------------------------------------------- /packages/sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true 6 | }, 7 | "compilerOptions": { 8 | "target": "ES6", 9 | "module": "ES2022", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "declaration": false, 13 | "esModuleInterop": true, 14 | "baseUrl": ".", 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"], 18 | "exclude": [] 19 | } 20 | -------------------------------------------------------------------------------- /packages/docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/CommandNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import type { Arguments } from "yargs-parser"; 2 | 3 | export class CommandNotFoundError extends Error { 4 | public name = "CommandNotFoundError"; 5 | 6 | constructor(private parsedArgs: Arguments) { 7 | super(); 8 | } 9 | 10 | get message(): string { 11 | return `Command not found with parsed args: ${JSON.stringify( 12 | this.parsedArgs, 13 | null, 14 | 2 15 | )}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-advanced/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true 6 | }, 7 | "compilerOptions": { 8 | "target": "ES6", 9 | "module": "ES2022", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "declaration": false, 13 | "esModuleInterop": true, 14 | "baseUrl": ".", 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"], 18 | "exclude": [] 19 | } 20 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-simple/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true 6 | }, 7 | "compilerOptions": { 8 | "target": "ES6", 9 | "module": "ES2022", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "declaration": false, 13 | "esModuleInterop": true, 14 | "baseUrl": ".", 15 | "outDir": "dist" 16 | }, 17 | "include": ["src"], 18 | "exclude": [] 19 | } 20 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Utils.struct.ts: -------------------------------------------------------------------------------- 1 | import type { InstanceFieldDecorationTypesUnion } from "../Components/Constants"; 2 | 3 | export interface BasePlaceholder { 4 | type: InstanceFieldDecorationTypesUnion; 5 | optionName?: string; 6 | optionAlias?: string; 7 | description?: string; 8 | initValue?: unknown; 9 | } 10 | 11 | export type TaggedDecoratedInstanceFields = { 12 | key: string; 13 | type: InstanceFieldDecorationTypesUnion; 14 | value: BasePlaceholder; 15 | }; 16 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/MultiRootCommandError.ts: -------------------------------------------------------------------------------- 1 | import type { ClassStruct } from "../Typings/Shared.struct"; 2 | 3 | export class MultiRootCommandError extends Error { 4 | public name = "MultiRootCommandError"; 5 | 6 | constructor( 7 | private existClass: ClassStruct, 8 | private incomingClass: ClassStruct 9 | ) { 10 | super(); 11 | } 12 | 13 | get message(): string { 14 | return `Multiple root command detected, RootCommand ${this.existClass.name} was already registered, and now ${this.incomingClass.name} is also registered as root command`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-mustard-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true 6 | }, 7 | "compilerOptions": { 8 | "target": "ES6", 9 | "module": "ES2022", 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "declaration": false, 14 | "noUnusedLocals": true, 15 | "resolveJsonModule": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "baseUrl": ".", 19 | "outDir": "dist" 20 | }, 21 | "include": ["source/index.mts"], 22 | "exclude": [] 23 | } 24 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/Constants.ts: -------------------------------------------------------------------------------- 1 | export type InstanceFieldDecorationTypesUnion = 2 | typeof MustardConstanst.InstanceFieldDecorationTypes[number]; 3 | 4 | export class MustardConstanst { 5 | public static InstanceFieldDecorationTypes = [ 6 | "Option", 7 | "Options", 8 | "VariadicOption", 9 | "Input", 10 | "Context", 11 | "Utils", 12 | "Inject", 13 | ]; 14 | 15 | public static RootCommandRegistryKey = "root"; 16 | 17 | public static InternalHelpFlag = "MUSTARD_SPECIFIED_HELP_FLAG"; 18 | 19 | public static InternalVersionFlag = "MUSTARD_SPECIFIED_VERSION_FLAG"; 20 | } 21 | -------------------------------------------------------------------------------- /packages/docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: "MustardCLI", 4 | description: "IoC & Native ES Decorator based command-line app builder.", 5 | url: "https://github.com/LinbuduLab", 6 | image: "/logo.png", 7 | socials: { 8 | github: "LinbuduLab/Mustard", 9 | }, 10 | github: { 11 | edit: true, 12 | }, 13 | header: { 14 | title: "LinbuduLab", 15 | logo: false, 16 | showLinkIcon: true, 17 | }, 18 | footer: { 19 | credits: { 20 | icon: "IconDocus", 21 | text: "Powered by Docus", 22 | href: "https://docus.com", 23 | }, 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Fixtures/UsageFixtures.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory, Context, MustardUtils } from "../../Exports"; 2 | import { 3 | Command, 4 | RootCommand, 5 | Option, 6 | VariadicOption, 7 | App, 8 | Ctx, 9 | Input, 10 | Inject, 11 | Utils, 12 | Options, 13 | } from "../../Exports/Decorators"; 14 | import { Validator } from "../../Exports/Validator"; 15 | import { CommandStruct, MustardApp } from "../../Exports/ComanndLine"; 16 | 17 | @RootCommand() 18 | export class RootCommandHandle implements CommandStruct { 19 | @Option("d") 20 | public msg = "default value of msg"; 21 | 22 | public run(): void { 23 | console.log("Root Command! ", this.msg); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/UnknownOptionsError.ts: -------------------------------------------------------------------------------- 1 | export class UnknownOptionsError extends Error { 2 | public name = "UnknownOptionsError"; 3 | 4 | constructor(private unknownOptions: string[]) { 5 | super(); 6 | } 7 | 8 | get message(): string { 9 | return `Unknown options: ${this.unknownOptions.join( 10 | ", " 11 | )}. See --help for usage.`; 12 | } 13 | } 14 | 15 | export class DidYouMeanError extends Error { 16 | public name = "DidYouMeanError"; 17 | 18 | constructor(private unknownOption: string, private didYouMean: string) { 19 | super(); 20 | } 21 | 22 | get message(): string { 23 | return `Unknown option --${this.unknownOption}, did you mean --${this.didYouMean}?`; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Temp.ts: -------------------------------------------------------------------------------- 1 | export type AnyClassDecoratorReturnType = ( 2 | target: any, 3 | context: ClassDecoratorContext 4 | ) => void; 5 | 6 | export type AnyClassMethodDecoratorReturnType = ( 7 | self: Function, 8 | context: ClassMemberDecoratorContext 9 | ) => Function | void; 10 | 11 | export type AnyClassGetterDecoratorReturnType = ( 12 | self: Function, 13 | context: ClassMemberDecoratorContext 14 | ) => Function | void; 15 | 16 | export type AnyClassSetterDecoratorReturnType = ( 17 | self: Function, 18 | context: ClassMemberDecoratorContext 19 | ) => Function | void; 20 | 21 | export type AnyClassFieldDecoratorReturnType = ( 22 | a1: undefined, 23 | context: ClassFieldDecoratorContext 24 | ) => (initialValue: any) => any | void; 25 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/BuiltInCommands/Usage1.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { RootCommand, App } from "../../../Exports/Decorators"; 3 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 4 | 5 | @RootCommand() 6 | class RootCommandHandle implements CommandStruct { 7 | public run(): void {} 8 | } 9 | 10 | @App({ 11 | name: "create-mustard-app", 12 | commands: [RootCommandHandle], 13 | configurations: { 14 | allowUnknownOptions: true, 15 | enableVersion() { 16 | return "10.11.0"; 17 | }, 18 | enableUsage: false, 19 | }, 20 | providers: [], 21 | }) 22 | class Project implements MustardApp { 23 | onStart() {} 24 | 25 | onComplete() {} 26 | } 27 | 28 | MustardFactory.init(Project).start(); 29 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Validators/EnumValidators.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import type { ZodNativeEnum } from "zod"; 4 | import type { Dictionary } from "../Typings/Shared.struct"; 5 | import type { MaybeOptionalZodType } from "./Typings"; 6 | 7 | type CommonEnumType = ZodNativeEnum>; 8 | 9 | export class NativeEnumValidator { 10 | _schema: CommonEnumType; 11 | 12 | constructor( 13 | private required: boolean = false, 14 | private enumValues: Dictionary 15 | ) { 16 | this._schema = z.nativeEnum(this.enumValues); 17 | } 18 | 19 | public get schema(): MaybeOptionalZodType { 20 | return this.required ? this._schema : this._schema.optional(); 21 | } 22 | 23 | public validate(value: unknown) { 24 | return this._schema.parse(value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/mustard-cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { configDefaults, defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | testTimeout: 300000, 6 | environment: "node", 7 | passWithNoTests: true, 8 | include: [ 9 | process.env.TEST_TYPE === "INTEGRATION" 10 | ? "source/__tests__/Integrations/**/*.{spec,test}.ts" 11 | : "source/__tests__/**/*.{spec,test}.ts", 12 | ], 13 | exclude: [...configDefaults.exclude].concat( 14 | process.env.TEST_TYPE === "UNIT" ? ["source/__tests__/Integrations"] : [] 15 | ), 16 | coverage: { 17 | enabled: true, 18 | reporter: ["text", "html", "json"], 19 | include: ["source/**/*.ts"], 20 | exclude: ["source/__tests__", "source/Errors/ValidationError.ts"], 21 | }, 22 | threads: true, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/3.Utils.md: -------------------------------------------------------------------------------- 1 | # Built-in Utils 2 | 3 | Mustard provides several tools that are frequently used in command-line application development, including **JSON file reading and writing**, **output coloring** and more in the future. These built-in utils do not cause severe frame size expansion but helps to reduce some installation costs during development. 4 | 5 | You can use these utils with `@Utils` decorator, and it will be injected into the command class as a property. 6 | 7 | ```typescript 8 | import { Command, Utils } from "mustard-cli/decorator"; 9 | import type { MustardUtils } from "mustard-cli"; 10 | 11 | 12 | @Command('run') 13 | class RunCommandHandle implements CommandStruct { 14 | @Utils() 15 | public utils!: MustardUtils; 16 | 17 | public run() { 18 | this.utils.json.readSync('package.json'); 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /packages/mustard-cli/source/Exports/Decorators.ts: -------------------------------------------------------------------------------- 1 | import { BuiltInDecorators } from "../Decorators/BuiltIn"; 2 | import { CommandDecorators } from "../Decorators/Command"; 3 | import { InputDecorator } from "../Decorators/Input"; 4 | import { OptionDecorators } from "../Decorators/Option"; 5 | import { DIServiceDecorators } from "../Decorators/DIService"; 6 | import { ControllerDecorators } from "../Decorators/Controller"; 7 | 8 | import { MustardFactory } from "../Components/MustardFactory"; 9 | 10 | export const { App } = MustardFactory; 11 | export const { Command, RootCommand } = CommandDecorators; 12 | export const { Option, Options, VariadicOption } = OptionDecorators; 13 | export const { Input } = InputDecorator; 14 | export const { Provide, Inject } = DIServiceDecorators; 15 | export const { Ctx, Utils } = BuiltInDecorators; 16 | export const { Restrict } = ControllerDecorators; 17 | -------------------------------------------------------------------------------- /packages/docs/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: MustardCLI 3 | navigation: false 4 | layout: page 5 | --- 6 | 7 | :ellipsis{right=0px width=75% blur=150px} 8 | 9 | ::block-hero 10 | --- 11 | 12 | cta: 13 | - Get Started → 14 | - /intro 15 | secondary: 16 | - View GitHub → 17 | - https://github.com/LinbuduLab/Mustard 18 | snippet: npx create-mustard-app 19 | --- 20 | 21 | #title 22 | Mustard CLI :badge[v1.0.0] 23 | 24 | #description 25 | [IoC](https://en.wikipedia.org/wiki/Inversion_of_control) & [Native ES Decorator](https://github.com/tc39/proposal-decorators) based command-line app builder. 26 | 27 | #extra 28 | ::list 29 | - Born to be **type safe** 30 | - Validator support by [Zod](https://github.com/colinhacks/zod) 31 | - Automatic usage info generation 32 | - **Build decoupled applications using IoC concepts** 33 | - Essential built-in utils for CLI app 34 | :: 35 | 36 | :: 37 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Exports/index.ts: -------------------------------------------------------------------------------- 1 | import { MustardUtilsProvider } from "../Components/MustardUtilsProvider"; 2 | 3 | export * from "./Decorators"; 4 | export * from "./ComanndLine"; 5 | export { Validator } from "./Validator"; 6 | export { MustardFactory } from "../Components/MustardFactory"; 7 | 8 | export type MustardUtils = Omit; 9 | export type { Context } from "../Typings/Context.struct"; 10 | 11 | export type * from "../Typings/Command.struct"; 12 | export type * from "../Typings/Configuration.struct"; 13 | export type * from "../Typings/Context.struct"; 14 | export type * from "../Typings/DIService.struct"; 15 | export type * from "../Typings/Factory.struct"; 16 | export type * from "../Typings/Option.struct"; 17 | export type * from "../Typings/Shared.struct"; 18 | export type * from "../Typings/Temp"; 19 | export type * from "../Typings/Utils.struct"; 20 | -------------------------------------------------------------------------------- /packages/create-mustard-app/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # create-mustard-app 6 | 7 | ## Requires 8 | 9 | - **Node.js >= 16.0.0** 10 | - **TypeScript >= 5.0.0** 11 | 12 | Before TypeScript 5.0 released, you may need to configure the used TypeScript version like below in `.vscode/settings.json`: 13 | 14 | ```json 15 | { 16 | "typescript.tsdk": "node_modules/typescript/lib" 17 | } 18 | ``` 19 | 20 | ## Getting Started 21 | 22 | Initialize a new project: 23 | 24 | ```bash 25 | npx create-mustard-app 26 | npm create mustard-app 27 | 28 | yarn create mustard-app 29 | 30 | pnpx create-mustard-app 31 | pnpm create mustard-app 32 | ``` 33 | 34 | Start the app: 35 | 36 | ```bash 37 | cd mustard-app 38 | 39 | npm install 40 | 41 | npm run dev 42 | # yarn dev 43 | # pnpm dev 44 | 45 | npm run start 46 | # yarn start 47 | # pnpm start 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/sample/samples/Nested.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { MustardFactory } from "mustard-cli"; 4 | import { Command, VariadicOption, App } from "mustard-cli/decorator"; 5 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 6 | 7 | @Command("dep", []) 8 | class UpdateDepCommand implements CommandStruct { 9 | @VariadicOption() 10 | public packages: string[] = []; 11 | 12 | public run(): void {} 13 | } 14 | 15 | @Command("update", [UpdateDepCommand]) 16 | class UpdateCommand implements CommandStruct { 17 | @VariadicOption() 18 | public packages: string[] = []; 19 | 20 | public run(): void {} 21 | } 22 | 23 | @App({ 24 | name: "LinbuduLab CLI", 25 | commands: [UpdateCommand], 26 | configurations: { 27 | allowUnknownOptions: true, 28 | }, 29 | providers: [], 30 | }) 31 | class Project implements MustardApp { 32 | onStart() {} 33 | 34 | onComplete() {} 35 | } 36 | 37 | MustardFactory.init(Project).start(); 38 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Decorators/DIService.ts: -------------------------------------------------------------------------------- 1 | import { MustardRegistry } from "../Components/Registry"; 2 | 3 | import type { InjectInitializerPlaceHolder } from "../Typings/Context.struct"; 4 | import type { 5 | AnyClassDecoratorReturnType, 6 | AnyClassFieldDecoratorReturnType, 7 | } from "../Typings/Temp"; 8 | 9 | /** 10 | * DI related decorators 11 | */ 12 | export class DIServiceDecorators { 13 | public static Inject(identifier?: string): AnyClassFieldDecoratorReturnType { 14 | return (_, context) => () => 15 | { 16 | type: "Inject", 17 | identifier: identifier ?? context.name, 18 | }; 19 | } 20 | 21 | public static Provide(identifier?: string): AnyClassDecoratorReturnType { 22 | return (target, context) => () => { 23 | MustardRegistry.ExternalProviderRegistry.set( 24 | identifier ?? context.name, 25 | 26 | target 27 | ); 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/BuiltInCommands/Usage2.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports"; 2 | import { RootCommand, App } from "../../../Exports/Decorators"; 3 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 4 | 5 | @RootCommand() 6 | class RootCommandHandle implements CommandStruct { 7 | public run(): void {} 8 | } 9 | 10 | @App({ 11 | name: "LinbuduLab CLI", 12 | commands: [RootCommandHandle], 13 | configurations: { 14 | allowUnknownOptions: true, 15 | enableVersion() { 16 | return "10.11.0"; 17 | }, 18 | enableUsage(registration) { 19 | if (!registration) { 20 | return "Usage: mustard [options] [command]"; 21 | } 22 | return `Usage: ${registration.commandInvokeName}`; 23 | }, 24 | }, 25 | providers: [], 26 | }) 27 | class Project implements MustardApp { 28 | onStart() {} 29 | 30 | onComplete() {} 31 | } 32 | 33 | MustardFactory.init(Project).start(); 34 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Command.struct.ts: -------------------------------------------------------------------------------- 1 | import type { MaybePromise, Constructable, Nullable } from "./Shared.struct"; 2 | import type { CommandList } from "./Configuration.struct"; 3 | import type { TaggedDecoratedInstanceFields } from "./Utils.struct"; 4 | 5 | export type CommandRegistryPayload = { 6 | commandInvokeName: string; 7 | Class: Constructable; 8 | root: boolean; 9 | childCommandList: CommandList; 10 | 11 | commandAlias?: Nullable; 12 | description?: Nullable; 13 | instance: CommandStruct; 14 | decoratedInstanceFields: TaggedDecoratedInstanceFields[]; 15 | }; 16 | 17 | export abstract class CommandStruct { 18 | abstract example?: () => string; 19 | 20 | abstract run(): MaybePromise; 21 | } 22 | 23 | export type CommandInput = [string, ...string[]]; 24 | 25 | export type CommandConfiguration = { 26 | name: string; 27 | alias?: string; 28 | description?: string; 29 | childCommandList?: CommandList; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/RootCommandOnly.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | } from "../../../Exports/Decorators"; 10 | import { CommandStruct } from "../../../Exports/ComanndLine"; 11 | 12 | @RootCommand() 13 | class RootCommandHandle implements CommandStruct { 14 | @Option("msg", "m") 15 | public msg = "default value of msg"; 16 | 17 | @Option() 18 | public notice = "default value of notice"; 19 | 20 | @VariadicOption({ alias: "p" }) 21 | public projects: string[] = []; 22 | 23 | @Input("description of inputs") 24 | public these_are_inputs: string; 25 | 26 | @Options() 27 | public options: unknown; 28 | 29 | public run(): void {} 30 | } 31 | 32 | @App({ 33 | name: "mm", 34 | commands: [RootCommandHandle], 35 | }) 36 | class Project {} 37 | 38 | MustardFactory.init(Project).start(); 39 | -------------------------------------------------------------------------------- /experimentals/PackageStarter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-container", 3 | "version": "0.0.1", 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org/" 7 | }, 8 | "scripts": { 9 | "local": "nodemon index.ts run sync --dry", 10 | "dev": "tsc --watch", 11 | "build": "tsc", 12 | "prepublishOnly": "pnpm run build" 13 | }, 14 | "nodemonConfig": { 15 | "delay": 500, 16 | "env": { 17 | "NODE_ENV": "development" 18 | }, 19 | "execMap": { 20 | "ts": "ts-node-esm" 21 | }, 22 | "ext": "ts,json", 23 | "ignore": [ 24 | "**/test/**", 25 | "**/docs/**", 26 | "node_modules" 27 | ], 28 | "restartable": "rs", 29 | "verbose": true, 30 | "watch": [ 31 | "*.ts" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^18.11.7", 36 | "nodemon": "^2.0.20", 37 | "ts-node": "^10.9.1", 38 | "tsconfig-paths": "^4.1.0", 39 | "typescript": "5.3.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /experimentals/MudtardContainer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-container", 3 | "version": "0.0.1", 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org/" 7 | }, 8 | "scripts": { 9 | "local": "nodemon index.ts run sync --dry", 10 | "dev": "tsc --watch", 11 | "build": "tsc", 12 | "prepublishOnly": "pnpm run build" 13 | }, 14 | "nodemonConfig": { 15 | "delay": 500, 16 | "env": { 17 | "NODE_ENV": "development" 18 | }, 19 | "execMap": { 20 | "ts": "ts-node-esm" 21 | }, 22 | "ext": "ts,json", 23 | "ignore": [ 24 | "**/test/**", 25 | "**/docs/**", 26 | "node_modules" 27 | ], 28 | "restartable": "rs", 29 | "verbose": true, 30 | "watch": [ 31 | "*.ts" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^18.11.7", 36 | "nodemon": "^2.0.20", 37 | "ts-node": "^10.9.1", 38 | "tsconfig-paths": "^4.1.0", 39 | "typescript": "5.3.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-project", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "pnpm --filter mustard-* build", 7 | "dev": "pnpm --filter mustard-cli dev", 8 | "release": "pnpm --filter mustard-cli publish --no-git-checks", 9 | "test": "pnpm --filter mustard-cli test -- -u" 10 | }, 11 | "nodemonConfig": { 12 | "delay": 500, 13 | "env": { 14 | "NODE_ENV": "development" 15 | }, 16 | "execMap": { 17 | "ts": "ts-node-esm" 18 | }, 19 | "ext": "ts,json", 20 | "ignore": [ 21 | "**/test/**", 22 | "**/docs/**", 23 | "node_modules" 24 | ], 25 | "restartable": "rs", 26 | "verbose": true, 27 | "watch": [ 28 | "*.ts" 29 | ] 30 | }, 31 | "prettier": {}, 32 | "dependencies": { 33 | "@changesets/cli": "^2.26.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^18.11.7", 37 | "nodemon": "^2.0.20", 38 | "ts-node": "^10.9.1", 39 | "tsconfig-paths": "^4.1.0", 40 | "typescript": "^5.3.3" 41 | } 42 | } -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/NoRootCommandButAtLeastOneCommandProvided.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | Command, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct } from "../../../Exports/ComanndLine"; 12 | 13 | @Command("update", "u", "update command") 14 | class UpdateCommandHandle implements CommandStruct { 15 | @Option("msg", "m") 16 | public msg = "default value of msg"; 17 | 18 | @Option() 19 | public notice = "default value of notice"; 20 | 21 | @VariadicOption({ alias: "p" }) 22 | public projects: string[] = []; 23 | 24 | @Input("description of inputs") 25 | public these_are_inputs: string; 26 | 27 | @Options() 28 | public options: unknown; 29 | 30 | public run(): void {} 31 | } 32 | 33 | @App({ 34 | name: "mm", 35 | commands: [UpdateCommandHandle], 36 | }) 37 | class Project {} 38 | 39 | MustardFactory.init(Project).start(); 40 | -------------------------------------------------------------------------------- /packages/mustard-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true, 6 | "require": ["tsconfig-paths/register"] 7 | }, 8 | "compilerOptions": { 9 | "lib": [], 10 | "target": "ES6", 11 | "module": "ES2022", 12 | "outDir": "dist", 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "strictNullChecks": true, 16 | "declaration": true, 17 | "strict": true, 18 | "noImplicitAny": true, 19 | "declarationMap": true, 20 | "noEmitOnError": true, 21 | "noImplicitReturns": true, 22 | "noUnusedParameters": false, 23 | "noImplicitThis": true, 24 | "noUnusedLocals": false, 25 | "strictPropertyInitialization": false, 26 | "esModuleInterop": true, 27 | "allowSyntheticDefaultImports": true, 28 | "baseUrl": ".", 29 | "rootDir": "source" 30 | }, 31 | "include": ["source"], 32 | "exclude": [ 33 | "dist", 34 | "samples", 35 | "source/__tests__", 36 | "vitest.config.ts", 37 | "sample.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /experimentals/MudtardContainer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true, 6 | "require": ["tsconfig-paths/register"] 7 | }, 8 | "compilerOptions": { 9 | "lib": [], 10 | "target": "ES6", 11 | "module": "CommonJS", 12 | "outDir": "dist", 13 | "skipLibCheck": true, 14 | "strictNullChecks": true, 15 | "declaration": true, 16 | "strict": true, 17 | "noImplicitAny": true, 18 | "noEmitOnError": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noUncheckedIndexedAccess": true, 23 | "noUnusedParameters": false, 24 | "noImplicitThis": true, 25 | "noUnusedLocals": false, 26 | "strictPropertyInitialization": false, 27 | "keyofStringsOnly": false, 28 | "esModuleInterop": true, 29 | "allowSyntheticDefaultImports": true, 30 | "baseUrl": ".", 31 | "rootDir": ".", 32 | "paths": {} 33 | }, 34 | "include": ["./**/*.ts"], 35 | "exclude": ["dist"] 36 | } 37 | -------------------------------------------------------------------------------- /experimentals/PackageStarter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": false, 4 | "pretty": true, 5 | "transpileOnly": true, 6 | "require": ["tsconfig-paths/register"] 7 | }, 8 | "compilerOptions": { 9 | "lib": [], 10 | "target": "ES6", 11 | "module": "CommonJS", 12 | "outDir": "dist", 13 | "skipLibCheck": true, 14 | "strictNullChecks": true, 15 | "declaration": true, 16 | "strict": true, 17 | "noImplicitAny": true, 18 | "noEmitOnError": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noUncheckedIndexedAccess": true, 23 | "noUnusedParameters": false, 24 | "noImplicitThis": true, 25 | "noUnusedLocals": false, 26 | "strictPropertyInitialization": false, 27 | "keyofStringsOnly": false, 28 | "esModuleInterop": true, 29 | "allowSyntheticDefaultImports": true, 30 | "baseUrl": ".", 31 | "rootDir": ".", 32 | "paths": {} 33 | }, 34 | "include": ["./**/*.ts"], 35 | "exclude": ["dist"] 36 | } 37 | -------------------------------------------------------------------------------- /packages/sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-sample", 3 | "private": true, 4 | "scripts": { 5 | "dev": "nodemon index.mts" 6 | }, 7 | "type": "module", 8 | "nodemonConfig": { 9 | "delay": 500, 10 | "execMap": { 11 | "js": "node --experimental-specifier-resolution=node", 12 | "ts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm", 13 | "mts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm" 14 | }, 15 | "ext": "ts,mts,js,mjs,json", 16 | "ignore": [ 17 | "**/test/**", 18 | "**/docs/**", 19 | "node_modules" 20 | ], 21 | "verbose": true, 22 | "watch": [ 23 | "*.ts", 24 | "*.mts", 25 | "*.js", 26 | "*.mjs" 27 | ] 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^18.16.2", 31 | "chalk": "^5.2.0", 32 | "nodemon": "^2.0.22", 33 | "ts-node": "^10.9.1", 34 | "tsconfig-paths": "^4.2.0", 35 | "typescript": "^5.3.3" 36 | }, 37 | "dependencies": { 38 | "mustard-cli": "workspace:*" 39 | }, 40 | "version": null 41 | } 42 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Validators/Factory.ts: -------------------------------------------------------------------------------- 1 | import { NativeEnumValidator } from "./EnumValidators"; 2 | import { 3 | StringValidator, 4 | BooleanValidator, 5 | NumberValidator, 6 | } from "./PrimitiveValidators"; 7 | 8 | import type { Dictionary, Nullable } from "../Typings/Shared.struct"; 9 | import type { ZodType } from "zod"; 10 | 11 | export class ValidatorFactory { 12 | public schema: Nullable = null; 13 | 14 | constructor(public required: boolean = false) {} 15 | 16 | public Required() { 17 | return new ValidatorFactory(true); 18 | } 19 | 20 | public Optional() { 21 | return new ValidatorFactory(false); 22 | } 23 | 24 | public String(): StringValidator { 25 | return new StringValidator(this.required); 26 | } 27 | 28 | public Boolean(): BooleanValidator { 29 | return new BooleanValidator(this.required); 30 | } 31 | 32 | public Number(): NumberValidator { 33 | return new NumberValidator(this.required); 34 | } 35 | 36 | public Enum(input: Dictionary): NativeEnumValidator { 37 | return new NativeEnumValidator(this.required, input); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import chalk from "chalk"; 3 | 4 | import type { ZodInvalidTypeIssue } from "zod"; 5 | 6 | export class ValidationError extends Error { 7 | public name = "ValidationError"; 8 | 9 | constructor( 10 | private invalidOptionName: string, 11 | private invalidOptionValue: unknown, 12 | private msg: string 13 | ) { 14 | super(); 15 | this.stack = undefined; 16 | } 17 | 18 | get message(): string { 19 | return chalk.yellow( 20 | `Invalid input for option ${chalk.bold(this.invalidOptionName)}` 21 | ); 22 | } 23 | 24 | public static formatError(argName: string, error: z.ZodError) { 25 | const issue = error.issues[0]; 26 | 27 | const { expected, received, message } = issue; 28 | 29 | if (expected && received) { 30 | return `Invalid input for argument '${argName}', expected: ${chalk.green( 31 | expected 32 | )}, received: ${chalk.yellow(received)}`; 33 | } else { 34 | return message ?? `Invalid input for argument '${argName}`; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Option.struct.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from "zod"; 2 | 3 | import type { ClassStruct } from "./Shared.struct"; 4 | import type { ValidatorFactory } from "../Validators/Factory"; 5 | import type { RestrictValueSet } from "./Controller.struct"; 6 | 7 | export type OptionInjectionType = "VariadicOption" | "Option" | "Options"; 8 | 9 | export type OptionRegistryPayload = { 10 | optionName: string; 11 | commandName: string; 12 | class: ClassStruct; 13 | }; 14 | 15 | export type OptionInitializerPlaceHolder = { 16 | type: OptionInjectionType; 17 | optionName?: string; 18 | optionAlias?: string; 19 | initValue?: unknown; 20 | description?: string; 21 | schema?: ZodType; 22 | restrictValues?: RestrictValueSet; 23 | }; 24 | 25 | export type OptionConfiguration = { 26 | name?: string; 27 | alias?: string; 28 | description?: string; 29 | validator?: Partial; 30 | }; 31 | 32 | export type InputConfiguration = Pick; 33 | 34 | export type VariadicOptionConfiguration = Omit< 35 | OptionConfiguration, 36 | "validator" 37 | >; 38 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Configuration.struct.ts: -------------------------------------------------------------------------------- 1 | import type { CommandRegistryPayload, CommandStruct } from "./Command.struct"; 2 | import type { Provider } from "./DIService.struct"; 3 | import type { MaybeFactory } from "./Shared.struct"; 4 | 5 | export interface Configurations { 6 | allowUnknownOptions?: boolean; 7 | debug: boolean; 8 | enableUsage: boolean | ((registration?: CommandRegistryPayload) => string); 9 | enableVersion: false | MaybeFactory; 10 | ignoreValidationErrors: boolean; 11 | defaultOverrides: boolean; 12 | lifeCycles?: Partial; 13 | didYouMean?: boolean; 14 | providers?: Provider[]; 15 | } 16 | 17 | export interface LifeCycles { 18 | onStart: () => void; 19 | onError: (err: Error) => void; 20 | onComplete: () => void; 21 | } 22 | 23 | export interface CLIInstantiationConfiguration 24 | extends Partial {} 25 | 26 | export type CommandList = (typeof CommandStruct)[]; 27 | 28 | export interface AppFactoryOptions { 29 | name?: string; 30 | commands: CommandList; 31 | configurations?: Partial; 32 | providers?: Provider[]; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 LinbuduLab 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. 22 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Context.struct.ts: -------------------------------------------------------------------------------- 1 | export type PackageManagerUtils = { 2 | install: () => void; 3 | uninstall: () => void; 4 | update: () => void; 5 | getUsingPackageManager: () => void; 6 | }; 7 | 8 | export type JSONUtils = { 9 | readSync: () => void; 10 | read: () => Promise; 11 | 12 | writeSync: () => void; 13 | write: () => Promise; 14 | }; 15 | 16 | export type BuiltInUtils = { 17 | pm: PackageManagerUtils; 18 | json: JSONUtils; 19 | }; 20 | 21 | export type Context = { 22 | cwd: string; 23 | argv: string[]; 24 | inputArgv: string[]; 25 | env: NodeJS.ProcessEnv; 26 | }; 27 | 28 | export type ContextInitializerPlaceHolder = { 29 | type: "Context"; 30 | }; 31 | 32 | export type InputInitializerPlaceHolder = { 33 | type: "Input"; 34 | }; 35 | 36 | export type UtilsInitializerPlaceHolder = { 37 | type: "Utils"; 38 | }; 39 | 40 | export type InjectInitializerPlaceHolder = { 41 | type: "Inject"; 42 | identifier: string; 43 | }; 44 | 45 | export type ProvideInitializerPlaceHolder = { 46 | type: "Provide"; 47 | identifier: string; 48 | context: ClassDecoratorContext; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/mustard-cli/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | IoC & [Native ECMAScript Decorator](https://github.com/tc39/proposal-decorators) based command line app builder. 6 | 7 | ## Requires 8 | 9 | - **Node.js >= 16.0.0** 10 | - **TypeScript >= 5.0.0** 11 | 12 | Before TypeScript 5.0 released, you may need to configure the used TypeScript version like below in `.vscode/settings.json`: 13 | 14 | ```json 15 | { 16 | "typescript.tsdk": "node_modules/typescript/lib" 17 | } 18 | ``` 19 | 20 | ## Features 21 | 22 | - Born to be type safe 23 | - Nest command support 24 | - Validator support by [Zod](https://github.com/colinhacks/zod) 25 | - Automatic usage info generation 26 | - Build decoupled applications using IoC concepts 27 | - Essential built-in utils for CLI app 28 | 29 | ## Getting Started 30 | 31 | - [Documentation](https://mustard-cli.netlify.app/) 32 | 33 | > Complete documentation will be provided after TypeScript 5.0 is officially released. 34 | 35 | ## Samples 36 | 37 | You can find more samples [Here](packages/sample/samples/). 38 | 39 | ## License 40 | 41 | [MIT](LICENSE) 42 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-simple/src/index.mts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module"; 2 | 3 | import { MustardFactory, MustardUtils } from "mustard-cli"; 4 | import { RootCommand, Option, App, Utils, Input } from "mustard-cli/decorator"; 5 | import type { CommandStruct, MustardApp } from "mustard-cli/cli"; 6 | 7 | const require = createRequire(import.meta.url); // construct the require method 8 | 9 | @RootCommand() 10 | class RootCommandHandle implements CommandStruct { 11 | @Option("dry", "d", "dry run command to see what will happen") 12 | public dry = false; 13 | 14 | @Utils() 15 | public utils!: MustardUtils; 16 | 17 | @Input() 18 | public input: string = "default"; 19 | 20 | public run(): void { 21 | console.log( 22 | "awesome-mustard-app", 23 | this.utils.colors.white(`v ${require("../package.json").version}\n`) 24 | ); 25 | console.log(`Input: ${this.input}`); 26 | } 27 | } 28 | 29 | @App({ 30 | name: "awesome-mustard-app", 31 | commands: [RootCommandHandle], 32 | }) 33 | class Project implements MustardApp { 34 | onError(error: Error): void { 35 | console.log(error); 36 | } 37 | } 38 | 39 | MustardFactory.init(Project).start(); 40 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Validators/Typings.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | z, 3 | ZodString, 4 | ZodNumber, 5 | ZodBoolean, 6 | ZodNativeEnum, 7 | ZodType, 8 | ZodOptional, 9 | } from "zod"; 10 | import type { Dictionary, ValidationTypes } from "../Typings/Shared.struct"; 11 | 12 | export type AvaliableSchemaValidations = 13 | | keyof typeof z 14 | | keyof ZodString 15 | | keyof ZodNumber 16 | | keyof ZodBoolean 17 | | keyof ZodNativeEnum>; 18 | 19 | export type ValidationItem< 20 | TTypes extends AvaliableSchemaValidations = AvaliableSchemaValidations 21 | > = { 22 | type: TTypes; 23 | args: unknown[]; 24 | }; 25 | 26 | export type MaybeOptionalZodType> = 27 | | T 28 | | ZodOptional; 29 | 30 | export abstract class BaseValidator< 31 | TValidationType extends ZodType, 32 | TParsedType extends unknown 33 | > { 34 | _schema!: TValidationType; 35 | 36 | constructor(required: boolean) {} 37 | 38 | abstract get schema(): MaybeOptionalZodType; 39 | 40 | abstract validate(value: unknown): TParsedType; 41 | 42 | abstract addValidation( 43 | type: ValidationTypes, 44 | args?: unknown[] 45 | ): void; 46 | } 47 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Decorators/BuiltIn.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ContextInitializerPlaceHolder, 3 | UtilsInitializerPlaceHolder, 4 | } from "../Typings/Context.struct"; 5 | import type { AnyClassFieldDecoratorReturnType } from "../Typings/Temp"; 6 | 7 | /** 8 | * Built-in providers related decorators 9 | */ 10 | export class BuiltInDecorators { 11 | /** 12 | * Inject utils 13 | * @example 14 | * class RunCommand { 15 | * \@Utils() 16 | * public utils: MustardUtils; 17 | * 18 | * run() { 19 | * this.utils.json.read(); 20 | * }; 21 | * } 22 | */ 23 | public static Utils(): AnyClassFieldDecoratorReturnType { 24 | return (_, context) => () => 25 | { 26 | type: "Utils", 27 | }; 28 | } 29 | 30 | /** 31 | * Inject context info 32 | * @example 33 | * class RunCommand { 34 | * \@Ctx() 35 | * public context: Context; 36 | * 37 | * run() { 38 | * this.context.stdout.write(); 39 | * }; 40 | * } 41 | */ 42 | public static Ctx(): AnyClassFieldDecoratorReturnType { 43 | return (_, context) => () => 44 | { 45 | type: "Context", 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/sample/samples/WithCustomProviders.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { MustardFactory } from "mustard-cli"; 4 | import { Command, App, Inject } from "mustard-cli/decorator"; 5 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 6 | 7 | import path from "path"; 8 | 9 | @Command("update") 10 | class UpdateCommand implements CommandStruct { 11 | @Inject("DataService") 12 | public data: DataService; 13 | 14 | @Inject("SharedService") 15 | public shared: SharedService; 16 | 17 | public run(): void {} 18 | } 19 | 20 | class DataService { 21 | public fetch() { 22 | return "FetchedData"; 23 | } 24 | } 25 | 26 | class SharedService { 27 | public execute() { 28 | return "ExecuteSharedService"; 29 | } 30 | } 31 | 32 | @App({ 33 | name: "create-mustard-app", 34 | commands: [UpdateCommand], 35 | configurations: { 36 | allowUnknownOptions: true, 37 | enableVersion: require(path.resolve("./package.json")).version, 38 | }, 39 | providers: [ 40 | SharedService, 41 | { 42 | identifier: DataService.name, 43 | value: DataService, 44 | }, 45 | ], 46 | }) 47 | class Project implements MustardApp { 48 | onStart() {} 49 | 50 | onComplete() {} 51 | } 52 | 53 | MustardFactory.init(Project).start(); 54 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Decorators/Controller.ts: -------------------------------------------------------------------------------- 1 | import { MustardUtils } from "../Components/Utils"; 2 | 3 | import type { RestrictValueSet } from "../Typings/Controller.struct"; 4 | import type { AnyClassFieldDecoratorReturnType } from "../Typings/Temp"; 5 | 6 | export class ControllerDecorators { 7 | /** 8 | * Restrict user input to a specific set of values 9 | * @param restrictValues 10 | * 11 | * @example 12 | * 13 | * const list = ['foo', 'bar', 'baz'] as const; 14 | * 15 | * type ListElements = typeof list[number]; 16 | * 17 | * class RunCommand { 18 | * 19 | * \@Restrict(list) 20 | * \@Option() 21 | * public value: ListElements = 'foo'; 22 | * } 23 | * 24 | * bin run --value=foo // foo 25 | * bin run --value=bar // bar 26 | * bin run --value=qux // foo 27 | */ 28 | public static Restrict( 29 | restrictValues: RestrictValueSet 30 | ): AnyClassFieldDecoratorReturnType { 31 | return (_, _context) => { 32 | return (initialValue) => { 33 | if (MustardUtils.isOptionInitializer(initialValue)) { 34 | return { 35 | ...initialValue, 36 | restrictValues, 37 | }; 38 | } 39 | 40 | return initialValue; 41 | }; 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/Root/Usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | } from "../../../Exports/Decorators"; 10 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 11 | 12 | @RootCommand() 13 | class RootCommandHandle implements CommandStruct { 14 | @Option("msg", "m") 15 | public msg = "default value of msg"; 16 | 17 | @VariadicOption({ alias: "p" }) 18 | public projects: string[] = []; 19 | 20 | @Input() 21 | public inputs: string; 22 | 23 | @Options() 24 | public options: unknown; 25 | 26 | public run(): void { 27 | console.log(`--msg option: ${this.msg}`); 28 | 29 | console.log(`--projects option: ${this.projects.join(",")}`); 30 | 31 | console.log(`inputs: ${this.inputs}`); 32 | 33 | console.log(`options: ${JSON.stringify(this.options)}`); 34 | } 35 | } 36 | 37 | @App({ 38 | name: "LinbuduLab CLI", 39 | commands: [RootCommandHandle], 40 | configurations: { 41 | allowUnknownOptions: true, 42 | }, 43 | providers: [], 44 | }) 45 | class Project implements MustardApp { 46 | onStart() {} 47 | 48 | onComplete() {} 49 | } 50 | 51 | MustardFactory.init(Project).start(); 52 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Decorators/Input.ts: -------------------------------------------------------------------------------- 1 | import type { AnyClassFieldDecoratorReturnType } from "../Typings/Temp"; 2 | import type { InputConfiguration } from "../Typings/Option.struct"; 3 | 4 | export class InputDecorator { 5 | /** 6 | * TODO: 7 | * 8 | * @Input(name, alias, description) 9 | * @Input({ name, alias, description }) 10 | * 11 | * Inject inputs after commands 12 | * @example 13 | * class RunCommand { 14 | * \@Input('list of projects to include') 15 | * public projects: string[]; 16 | * } 17 | */ 18 | public static Input(description?: string): AnyClassFieldDecoratorReturnType; 19 | 20 | /** 21 | * Inject inputs after commands 22 | * @example 23 | * class RunCommand { 24 | * \@Input({ description: 'list of projects to include' } }) 25 | * public projects: string[]; 26 | * } 27 | */ 28 | public static Input( 29 | configuration?: InputConfiguration 30 | ): AnyClassFieldDecoratorReturnType; 31 | public static Input( 32 | config?: string | InputConfiguration 33 | ): AnyClassFieldDecoratorReturnType { 34 | const inputDescription = 35 | typeof config === "string" ? config : config?.description; 36 | 37 | return (_, context) => (initValue) => { 38 | return { 39 | type: "Input", 40 | initValue, 41 | description: inputDescription, 42 | }; 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/UtilsProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | import tmp from "tmp"; 3 | 4 | import { 5 | JSONHelper, 6 | MustardUtilsProvider, 7 | } from "../Components/MustardUtilsProvider"; 8 | 9 | describe("UtilsProvider", () => { 10 | it("should provide internal utils", () => { 11 | const utils = MustardUtilsProvider.produce(); 12 | 13 | expect(utils.json).toBeDefined(); 14 | expect(utils.json.read).toBeDefined(); 15 | expect(utils.json.readSync).toBeDefined(); 16 | expect(utils.json.write).toBeDefined(); 17 | expect(utils.json.writeSync).toBeDefined(); 18 | }); 19 | }); 20 | 21 | describe("JSONHelper", () => { 22 | it("should read json file", async () => { 23 | const tmpFile1 = tmp.fileSync(); 24 | const tmpFile2 = tmp.fileSync(); 25 | const filePath1 = tmpFile1.name; 26 | const filePath2 = tmpFile1.name; 27 | const content = { foo: "bar" }; 28 | 29 | await JSONHelper.writeJson(filePath1, content); 30 | 31 | const parsed = await JSONHelper.readJson(filePath1); 32 | 33 | expect(parsed).toEqual(content); 34 | 35 | JSONHelper.writeJsonSync(filePath2, content); 36 | 37 | const parsed2 = JSONHelper.readJsonSync(filePath2); 38 | 39 | expect(parsed2).toEqual(content); 40 | 41 | tmpFile1.removeCallback(); 42 | tmpFile2.removeCallback(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/BuiltInCommands/Usage3.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { RootCommand, App, Command, Option } from "../../../Exports/Decorators"; 3 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 4 | 5 | @Command("update", "execute update command") 6 | class UpdateCommandHandle implements CommandStruct { 7 | @Option("name", "name of the project") 8 | public name: string; 9 | 10 | @Option("version", "version of the project") 11 | public version: string; 12 | 13 | public run(): void {} 14 | } 15 | 16 | @Command("sync", "execute update command") 17 | class SyncCommandHandle implements CommandStruct { 18 | @Option("name", "n", "name of the project") 19 | public name: string; 20 | 21 | @Option("type", "t", "type of the project") 22 | public type: string; 23 | 24 | public run(): void {} 25 | } 26 | 27 | @RootCommand() 28 | class RootCommandHandle implements CommandStruct { 29 | public run(): void {} 30 | } 31 | 32 | @App({ 33 | name: "LinbuduLab CLI", 34 | commands: [RootCommandHandle, UpdateCommandHandle, SyncCommandHandle], 35 | configurations: { 36 | allowUnknownOptions: true, 37 | enableVersion() { 38 | return "10.11.0"; 39 | }, 40 | }, 41 | providers: [], 42 | }) 43 | class Project implements MustardApp { 44 | onStart() {} 45 | 46 | onComplete() {} 47 | } 48 | 49 | MustardFactory.init(Project).start(); 50 | -------------------------------------------------------------------------------- /packages/sample/samples/Common.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { MustardFactory } from "mustard-cli"; 4 | import { 5 | Command, 6 | RootCommand, 7 | Option, 8 | VariadicOption, 9 | App, 10 | Input, 11 | Options, 12 | } from "mustard-cli/decorator"; 13 | import { Validator } from "mustard-cli/validator"; 14 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 15 | 16 | import path from "path"; 17 | 18 | @RootCommand() 19 | class RootCommandHandle implements CommandStruct { 20 | @Option("m") 21 | public msg = "default value of msg"; 22 | 23 | public run(): void { 24 | console.log("Root Command! ", this.msg); 25 | } 26 | } 27 | 28 | @Command("update", "u", "update project dependencies") 29 | class UpdateCommand implements CommandStruct { 30 | @Option("depth", Validator.Number().Gte(1)) 31 | public depth = 10; 32 | 33 | @Option(Validator.Boolean()) 34 | public dry = false; 35 | 36 | @Options() 37 | public completeOptions: unknown; 38 | 39 | @Input() 40 | public input: string[]; 41 | 42 | @VariadicOption() 43 | public packages: string[] = []; 44 | 45 | public run(): void {} 46 | } 47 | 48 | @App({ 49 | name: "LinbuduLab CLI", 50 | commands: [RootCommandHandle, UpdateCommand], 51 | configurations: { 52 | allowUnknownOptions: true, 53 | enableVersion: require(path.resolve("./package.json")).version, 54 | }, 55 | providers: [], 56 | }) 57 | class Project implements MustardApp { 58 | onStart() {} 59 | 60 | onComplete() {} 61 | } 62 | 63 | MustardFactory.init(Project).start(); 64 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Typings/Shared.struct.ts: -------------------------------------------------------------------------------- 1 | import type { ZodType } from "zod"; 2 | 3 | export type Dictionary = Record; 4 | 5 | export type FuncStruct< 6 | TArgs extends unknown[] = unknown[], 7 | TReturnType extends unknown = unknown 8 | > = (...args: TArgs) => Promise; 9 | 10 | export type ClassStruct = new ( 11 | ...args: any[] 12 | ) => TInstanceType; 13 | 14 | export type Nullable = T | null; 15 | 16 | export type MaybeFactory = T | ((...args: any[]) => T); 17 | 18 | export type MaybeArray = T | T[]; 19 | 20 | export type MaybeAsyncFactory = T | (() => Promise); 21 | 22 | export type MaybePromise = T | Promise; 23 | 24 | export type Constructable = new (...args: any[]) => T; 25 | 26 | export type ExpectedPropKeys = { 27 | [Key in keyof T]-?: T[Key] extends ValueType ? Key : never; 28 | }[keyof T]; 29 | 30 | export type CallableKeys = ExpectedPropKeys; 31 | 32 | export type ValidationTypes = Extract< 33 | keyof WritablePart, 34 | CallableKeys> 35 | >; 36 | 37 | type IfEquals = (() => T extends X ? 1 : 2) extends < 38 | T 39 | >() => T extends Y ? 1 : 2 40 | ? A 41 | : B; 42 | 43 | type WritableKeysOf = { 44 | [P in keyof T]: IfEquals< 45 | { [Q in P]: T[P] }, 46 | { -readonly [Q in P]: T[P] }, 47 | P, 48 | never 49 | >; 50 | }[keyof T]; 51 | 52 | type WritablePart = Pick>; 53 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/NoRootCommandButMultipleCommandsRegistered.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | Command, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct } from "../../../Exports/ComanndLine"; 12 | 13 | @Command("update", "u", "update command") 14 | class UpdateCommandHandle implements CommandStruct { 15 | @Option("msg", "m") 16 | public msg = "default value of msg"; 17 | 18 | @Option() 19 | public notice = "default value of notice"; 20 | 21 | @VariadicOption({ alias: "p" }) 22 | public projects: string[] = []; 23 | 24 | @Input("description of inputs") 25 | public these_are_inputs: string; 26 | 27 | @Options() 28 | public options: unknown; 29 | 30 | public run(): void {} 31 | } 32 | 33 | @Command("sync", "s", "sync command") 34 | class SyncCommandHandle implements CommandStruct { 35 | @Option("msg", "m") 36 | public msg = "default value of msg"; 37 | 38 | @Option() 39 | public notice = "default value of notice"; 40 | 41 | @VariadicOption({ alias: "p" }) 42 | public projects: string[] = []; 43 | 44 | @Input("description of inputs") 45 | public these_are_inputs: string; 46 | 47 | @Options() 48 | public options: unknown; 49 | 50 | public run(): void {} 51 | } 52 | 53 | @App({ 54 | name: "mm", 55 | commands: [UpdateCommandHandle, SyncCommandHandle], 56 | }) 57 | class Project {} 58 | 59 | MustardFactory.init(Project).start(); 60 | -------------------------------------------------------------------------------- /packages/mustard-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog for mustard-cli 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - bump typescript version to 5.3.3 8 | 9 | ## 1.1.0 10 | 11 | ### Minor Changes 12 | 13 | - bump typescript version 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - 292778d: 1.0.0 release 20 | 21 | ## 0.8.3 22 | 23 | ### Patch Changes 24 | 25 | - d6aca87: UsageGenerator enhancement 26 | - fdf73a3: fix usage generation in nested commands 27 | 28 | ## 0.8.2 29 | 30 | ### Patch Changes 31 | 32 | - 0479502: @Restrict support 33 | - bump ts version 34 | 35 | ## 0.8.1 36 | 37 | ### Patch Changes 38 | 39 | - Provide package-specified README 40 | 41 | ## 0.8.0 42 | 43 | ### Minor Changes 44 | 45 | - fix template dev workflow 46 | - enhance dev workflow 47 | 48 | ## 0.7.1 49 | 50 | ### Patch Changes 51 | 52 | - fixup package import path 53 | 54 | ## 0.7.0 55 | 56 | ### Minor Changes 57 | 58 | - 036f9b9: Fix starter dependencies 59 | 60 | ## 0.6.3 61 | 62 | ### Patch Changes 63 | 64 | - fixup \_\_dirname in es scope 65 | - 746ae49: Fix build config, use PURE ESM 66 | 67 | ## 0.6.0 68 | 69 | ### Minor Changes 70 | 71 | - use module:es2022 72 | 73 | ## 0.5.0 74 | 75 | ### Minor Changes 76 | 77 | - 4902a30: Fix required schema validation 78 | 79 | ### Patch Changes 80 | 81 | - Bump typescript version to 5.0.0 beta 82 | - 7c32d55: Fix UsageGenerator for all suits 83 | - 7302020: Configuration and Usage info collection for @Input 84 | 85 | ## 0.4.1 86 | 87 | ### Patch Changes 88 | 89 | - b340fc2: add colors for built-in utils 90 | - c469b63: Testing 91 | -------------------------------------------------------------------------------- /packages/create-mustard-app/template-advanced/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesome-mustard-app", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "main": "index.js", 6 | "bin": { 7 | "awesome-mustard-app": "index.js" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "registry": "https://registry.npmjs.org/" 12 | }, 13 | "files": [ 14 | "dist", 15 | "index.js" 16 | ], 17 | "scripts": { 18 | "dev": "nodemon src/index.mts", 19 | "start": "npm run build && nodemon index.js", 20 | "build": "tsc", 21 | "pub:check": "pnpx publint", 22 | "prepublishOnly": "pnpm run pub:check && pnpm run build" 23 | }, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | }, 27 | "nodemonConfig": { 28 | "delay": 500, 29 | "env": { 30 | "NODE_ENV": "development" 31 | }, 32 | "execMap": { 33 | "js": "node --experimental-specifier-resolution=node", 34 | "ts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm", 35 | "mts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm" 36 | }, 37 | "ext": "ts,mts,js,mjs,json", 38 | "ignore": [ 39 | "**/test/**", 40 | "**/docs/**", 41 | "node_modules" 42 | ], 43 | "restartable": "rs", 44 | "verbose": true, 45 | "watch": [ 46 | "*.ts", 47 | "*.mts", 48 | "*.js", 49 | "*.mjs" 50 | ] 51 | }, 52 | "dependencies": { 53 | "mustard-cli": "latest" 54 | }, 55 | "devDependencies": { 56 | "@types/node": "latest", 57 | "ts-node": "^10.9.1", 58 | "typescript": "^5.0.0" 59 | } 60 | } -------------------------------------------------------------------------------- /packages/create-mustard-app/template-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesome-mustard-app", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "main": "index.js", 6 | "bin": { 7 | "awesome-mustard-app": "index.js" 8 | }, 9 | "publishConfig": { 10 | "access": "public", 11 | "registry": "https://registry.npmjs.org/" 12 | }, 13 | "files": [ 14 | "dist", 15 | "index.js" 16 | ], 17 | "scripts": { 18 | "dev": "nodemon src/index.mts", 19 | "start": "npm run build && nodemon index.js", 20 | "build": "tsc", 21 | "pub:check": "pnpx publint", 22 | "prepublishOnly": "pnpm run pub:check && pnpm run build" 23 | }, 24 | "engines": { 25 | "node": ">=16.0.0" 26 | }, 27 | "nodemonConfig": { 28 | "delay": 500, 29 | "env": { 30 | "NODE_ENV": "development" 31 | }, 32 | "execMap": { 33 | "js": "node --experimental-specifier-resolution=node", 34 | "ts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm", 35 | "mts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm" 36 | }, 37 | "ext": "ts,mts,js,mjs,json", 38 | "ignore": [ 39 | "**/test/**", 40 | "**/docs/**", 41 | "node_modules" 42 | ], 43 | "restartable": "rs", 44 | "verbose": true, 45 | "watch": [ 46 | "*.ts", 47 | "*.mts", 48 | "*.js", 49 | "*.mjs" 50 | ] 51 | }, 52 | "dependencies": { 53 | "mustard-cli": "latest" 54 | }, 55 | "devDependencies": { 56 | "@types/node": "latest", 57 | "ts-node": "^10.9.1", 58 | "typescript": "^5.0.0" 59 | } 60 | } -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/1.Commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ## Configurations 4 | 5 | When registering a command, you can configure its trigger name, alias, description, and list of subcommands, which Mustard supports in the form of parameter lists or objects: 6 | 7 | ```typescript 8 | @Command('run') 9 | @Command('run', 'r') 10 | @Command('run', 'run command handle') 11 | @Command('run', 'r', 'run command handle') 12 | @Command('run', [RunSubCommandHandle]) 13 | @Command({ 14 | name: 'run', 15 | alias: 'r', 16 | description: 'run command handle', 17 | subCommands: [RunSubCommandHandle] 18 | }) 19 | class RunCommandHandle implements CommandStruct { 20 | public run() {} 21 | } 22 | ``` 23 | 24 | ::list{type="info"} 25 | To split `@Command('run', 'r')` and `@Command('run', 'run command handle')` overloads, if you provide a string which length is less than 3, it will be treated as alias, otherwise it will be treated as description. 26 | :: 27 | 28 | ## Nested Commands 29 | 30 | Mustard supports infinite levels of nested commands, and you can also use `@Command` decorator to register subcommands: 31 | 32 | ```typescript 33 | @Command('log') 34 | class RunSyncLogCommandHandle implements CommandStruct { 35 | public run() {} 36 | } 37 | 38 | @Command('sync', [RunSyncLogCommandHandle]) 39 | class RunSyncCommandHandle implements CommandStruct { 40 | public run() {} 41 | } 42 | 43 | @Command('update') 44 | class RunUpdateCommandHandle implements CommandStruct { 45 | public run() {} 46 | } 47 | 48 | @Command('run', [RunSyncCommandHandle, RunUpdateCommandHandle]) 49 | class RunCommandHandle implements CommandStruct { 50 | public run() {} 51 | } 52 | ``` -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/MustardFactory.ts: -------------------------------------------------------------------------------- 1 | import { CLI } from "../Commands/CommandLine"; 2 | import { NullishFactoryOptionError } from "../Errors/NullishFactoryOptionError"; 3 | 4 | import type { MustardApp } from "../Typings/Factory.struct"; 5 | import type { AppFactoryOptions } from "../Typings/Configuration.struct"; 6 | import type { Constructable, Nullable } from "../Typings/Shared.struct"; 7 | import type { AnyClassDecoratorReturnType } from "../Typings/Temp"; 8 | 9 | export class MustardFactory { 10 | private static FactoryOptions: Nullable = null; 11 | 12 | /** 13 | * Register application entry handler 14 | * @returns 15 | */ 16 | public static App( 17 | configuration: AppFactoryOptions 18 | ): AnyClassDecoratorReturnType { 19 | return () => { 20 | MustardFactory.FactoryOptions = configuration; 21 | }; 22 | } 23 | 24 | private static flush(): void { 25 | MustardFactory.FactoryOptions = null; 26 | } 27 | 28 | /** 29 | * Initialize application 30 | * @param Cls 31 | * @returns 32 | */ 33 | public static init(Cls: Constructable): CLI { 34 | if (!MustardFactory.FactoryOptions) throw new NullishFactoryOptionError(); 35 | 36 | const ins = new Cls(); 37 | 38 | const { 39 | name, 40 | commands, 41 | configurations = {}, 42 | 43 | providers = [], 44 | } = MustardFactory.FactoryOptions; 45 | 46 | const cli = new CLI(name ?? "", commands, configurations); 47 | 48 | cli.registerProvider(providers); 49 | 50 | cli.configure({ 51 | lifeCycles: { 52 | onStart: ins.onStart, 53 | onError: ins.onError, 54 | onComplete: ins.onComplete, 55 | }, 56 | }); 57 | 58 | MustardFactory.flush(); 59 | 60 | return cli; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/sample/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # mustard-sample 2 | 3 | ## null 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - mustard-cli@1.1.1 9 | 10 | ## null 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - mustard-cli@1.1.0 16 | 17 | ## null 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [292778d] 22 | - mustard-cli@1.0.0 23 | 24 | ## null 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [d6aca87] 29 | - Updated dependencies [fdf73a3] 30 | - mustard-cli@0.8.3 31 | 32 | ## null 33 | 34 | ### Patch Changes 35 | 36 | - Updated dependencies [0479502] 37 | - Updated dependencies 38 | - mustard-cli@0.8.2 39 | 40 | ## null 41 | 42 | ### Patch Changes 43 | 44 | - Updated dependencies 45 | - mustard-cli@0.8.1 46 | 47 | ## null 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies 52 | - Updated dependencies 53 | - mustard-cli@0.8.0 54 | 55 | ## null 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies 60 | - mustard-cli@0.7.1 61 | 62 | ## null 63 | 64 | ### Patch Changes 65 | 66 | - Updated dependencies [036f9b9] 67 | - mustard-cli@0.7.0 68 | 69 | ## null 70 | 71 | ### Patch Changes 72 | 73 | - Updated dependencies 74 | - Updated dependencies [746ae49] 75 | - mustard-cli@0.6.3 76 | 77 | ## null 78 | 79 | ### Patch Changes 80 | 81 | - Updated dependencies 82 | - mustard-cli@0.6.0 83 | 84 | ## null 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies [4902a30] 89 | - Updated dependencies 90 | - Updated dependencies [7c32d55] 91 | - Updated dependencies [7302020] 92 | - mustard-cli@0.5.0 93 | 94 | ## null 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies [b340fc2] 99 | - Updated dependencies [c469b63] 100 | - mustard-cli@0.4.1 101 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Registry.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | import { MustardRegistry } from "../Components/Registry"; 3 | 4 | describe("Registry", () => { 5 | it("should register and provide init payload", () => { 6 | const payload = { 7 | description: "foo", 8 | handler: () => {}, 9 | run: () => {}, 10 | }; 11 | 12 | MustardRegistry.registerInit("foo", payload); 13 | 14 | expect(MustardRegistry.provideInit("foo")).toEqual(payload); 15 | expect(Array.from(MustardRegistry.provideInit().keys())).toEqual(["foo"]); 16 | expect(Array.from(MustardRegistry.provideInit().values())).toEqual([ 17 | payload, 18 | ]); 19 | }); 20 | 21 | it("should register and provide", () => { 22 | const payload = { 23 | description: "foo", 24 | handler: () => {}, 25 | run: () => {}, 26 | }; 27 | 28 | MustardRegistry.register("foo", payload); 29 | 30 | expect(MustardRegistry.provide("foo")).toEqual(payload); 31 | expect(Array.from(MustardRegistry.provide().keys())).toEqual(["foo"]); 32 | expect(Array.from(MustardRegistry.provide().values())).toEqual([payload]); 33 | }); 34 | 35 | it("should upsert from registry", () => { 36 | const payload = { 37 | description: "foo", 38 | handler: () => {}, 39 | run: () => {}, 40 | }; 41 | 42 | MustardRegistry.register("foo", payload); 43 | 44 | MustardRegistry.upsert("foo", { description: "bar" }); 45 | MustardRegistry.upsert("bar", { description: "bar" }); 46 | 47 | expect(MustardRegistry.provide("foo").description).toBe("bar"); 48 | expect(MustardRegistry.provide("bar").description).toBe("bar"); 49 | }); 50 | 51 | it("should handle root", () => { 52 | MustardRegistry.register("root", { description: "root" }); 53 | 54 | expect(MustardRegistry.provideRootCommand()).toEqual({ 55 | description: "root", 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/RestrictValue/Usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | Restrict, 6 | App, 7 | } from "../../../Exports/Decorators"; 8 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 9 | 10 | const restrictArray = ["foo", "bar", "baz"] as const; 11 | 12 | const restrictObject = { 13 | foo: "foo", 14 | bar: "bar", 15 | baz: "baz", 16 | } as const; 17 | 18 | enum RestrictEnum { 19 | Foo = "foo", 20 | Bar = "bar", 21 | Baz = "baz", 22 | } 23 | 24 | type RestrictArrayType = typeof restrictArray[number]; 25 | type RestrictObjectType = typeof restrictObject[keyof typeof restrictObject]; 26 | 27 | @RootCommand() 28 | class RootCommandHandle implements CommandStruct { 29 | @Option() 30 | public notRestrict: string = "default value of notRestrict"; 31 | 32 | @Restrict(restrictArray) 33 | @Option() 34 | public restrictedArrayTypeOption: RestrictArrayType = "foo"; 35 | 36 | @Restrict(restrictObject) 37 | @Option() 38 | public restrictedObjectTypeOption: RestrictObjectType = "foo"; 39 | 40 | @Restrict(RestrictEnum) 41 | @Option() 42 | public restrictedEnumTypeOption: RestrictEnum = RestrictEnum.Foo; 43 | 44 | public run(): void { 45 | console.log("Root Command"); 46 | console.log(`--notRestrict option: ${this.notRestrict}`); 47 | console.log( 48 | `--restrictedArrayTypeOption option: ${this.restrictedArrayTypeOption}` 49 | ); 50 | console.log( 51 | `--restrictedObjectTypeOption option: ${this.restrictedObjectTypeOption}` 52 | ); 53 | console.log( 54 | `--restrictedEnumTypeOption option: ${this.restrictedEnumTypeOption}` 55 | ); 56 | } 57 | } 58 | 59 | @App({ 60 | name: "LinbuduLab CLI", 61 | commands: [RootCommandHandle], 62 | }) 63 | class Project implements MustardApp { 64 | onStart() {} 65 | 66 | onComplete() {} 67 | } 68 | 69 | MustardFactory.init(Project).start(); 70 | -------------------------------------------------------------------------------- /packages/create-mustard-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-mustard-app", 3 | "version": "1.1.1", 4 | "homepage": "https://github.com/LinbuduLab/Mustard/tree/main/packages/create-mustard-app#readme", 5 | "bugs": { 6 | "url": "https://github.com/LinbuduLab/create-mustard-app/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/LinbuduLab/Mustard.git", 11 | "directory": "packages/create-mustard-app" 12 | }, 13 | "type": "module", 14 | "main": "index.js", 15 | "bin": { 16 | "create-mustard-app": "index.js" 17 | }, 18 | "files": [ 19 | "index.js", 20 | "dist", 21 | "template-*" 22 | ], 23 | "scripts": { 24 | "dev": "run-p start build:watch", 25 | "start": "nodemon index.js", 26 | "build": "tsc", 27 | "build:watch": "tsc --watch", 28 | "pub:check": "pnpx publint", 29 | "prepublishOnly": "pnpm run pub:check && pnpm run build" 30 | }, 31 | "nodemonConfig": { 32 | "delay": 500, 33 | "env": { 34 | "NODE_ENV": "development" 35 | }, 36 | "execMap": { 37 | "js": "node --experimental-specifier-resolution=node", 38 | "ts": "ts-node-esm" 39 | }, 40 | "ext": "ts,mts,js,mjs,json", 41 | "ignore": [ 42 | "**/test/**", 43 | "**/docs/**", 44 | "node_modules" 45 | ], 46 | "restartable": "rs", 47 | "verbose": true, 48 | "watch": [ 49 | "*.ts", 50 | "*.mts", 51 | "*.js", 52 | "*.mjs" 53 | ] 54 | }, 55 | "dependencies": { 56 | "fs-extra": "^11.1.0", 57 | "log-symbols": "^5.1.0", 58 | "mustard-cli": "workspace:*" 59 | }, 60 | "devDependencies": { 61 | "@types/fs-extra": "^11.0.1", 62 | "@types/node": "^18.11.18", 63 | "npm-run-all": "^4.1.5", 64 | "ts-node": "^10.9.1", 65 | "typescript": "^5.3.3" 66 | }, 67 | "engines": { 68 | "node": ">=16.0.0" 69 | }, 70 | "publishConfig": { 71 | "access": "public", 72 | "registry": "https://registry.npmjs.org/" 73 | } 74 | } -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | App, 6 | Input, 7 | Options, 8 | } from "../../../Exports/Decorators"; 9 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 10 | 11 | @RootCommand() 12 | class RootCommandHandle1 implements CommandStruct { 13 | @Option() 14 | public pure = "default value of pure"; 15 | 16 | @Option("msg") 17 | public msgOption = "default value of msg"; 18 | 19 | @Input() 20 | public inputs: string; 21 | 22 | @Options() 23 | public options: unknown; 24 | 25 | public run(): void { 26 | console.log("Root Command"); 27 | 28 | console.log(`--pure option: ${this.pure}`); 29 | 30 | console.log(`--msg option: ${this.msgOption}`); 31 | 32 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 33 | 34 | console.log(`options: ${JSON.stringify(this.options)}`); 35 | } 36 | } 37 | @RootCommand() 38 | class RootCommandHandle2 implements CommandStruct { 39 | @Option() 40 | public pure = "default value of pure"; 41 | 42 | @Option("msg") 43 | public msgOption = "default value of msg"; 44 | 45 | @Input() 46 | public inputs: string; 47 | 48 | @Options() 49 | public options: unknown; 50 | 51 | public run(): void { 52 | console.log("Root Command"); 53 | 54 | console.log(`--pure option: ${this.pure}`); 55 | 56 | console.log(`--msg option: ${this.msgOption}`); 57 | 58 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 59 | 60 | console.log(`options: ${JSON.stringify(this.options)}`); 61 | } 62 | } 63 | 64 | @App({ 65 | name: "LinbuduLab CLI", 66 | commands: [RootCommandHandle1, RootCommandHandle2], 67 | configurations: { 68 | allowUnknownOptions: true, 69 | }, 70 | providers: [], 71 | }) 72 | class Project implements MustardApp { 73 | onStart() {} 74 | 75 | onComplete() {} 76 | } 77 | 78 | MustardFactory.init(Project).start(); 79 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/MustardFactory.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | 3 | import { MustardApp } from "../Exports/ComanndLine"; 4 | import { NullishFactoryOptionError } from "../Errors/NullishFactoryOptionError"; 5 | import { MustardFactory } from "../Components/MustardFactory"; 6 | 7 | const fn1 = vi.fn(); 8 | const fn2 = vi.fn(); 9 | const fn3 = vi.fn(); 10 | 11 | vi.mock("../Command/CommandLine.ts", () => { 12 | return { 13 | CLI: class CLI { 14 | constructor(...args: unknown[]) { 15 | fn1(...args); 16 | } 17 | registerProvider(providers: unknown[]) { 18 | fn2(providers); 19 | } 20 | configure(config: unknown) { 21 | fn3(config); 22 | } 23 | }, 24 | }; 25 | }); 26 | 27 | describe("Mustard Factory", () => { 28 | it("should throw on no config provided", () => { 29 | try { 30 | class Project implements MustardApp {} 31 | MustardFactory.init(Project); 32 | } catch (error) { 33 | expect(error).toBeInstanceOf(NullishFactoryOptionError); 34 | } 35 | }); 36 | it("should handle factory initialization", () => { 37 | @MustardFactory.App({ 38 | name: "Project", 39 | commands: [], 40 | configurations: { 41 | allowUnknownOptions: true, 42 | }, 43 | providers: [], 44 | }) 45 | class Project implements MustardApp {} 46 | 47 | MustardFactory.init(Project); 48 | 49 | // expect(fn1).toBeCalledWith("Project", [], { 50 | // allowUnknownOptions: true, 51 | // }); 52 | 53 | // expect(fn2).toBeCalledWith([]); 54 | 55 | // expect(fn3).toBeCalledWith({ 56 | // lifeCycles: { 57 | // onStart: undefined, 58 | // onError: undefined, 59 | // onComplete: undefined, 60 | // }, 61 | // }); 62 | 63 | try { 64 | MustardFactory.init(Project); 65 | } catch (error) { 66 | expect(error).toBeInstanceOf(NullishFactoryOptionError); 67 | } 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/RootCommandAndCommonCommandsProvided.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | Command, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct } from "../../../Exports/ComanndLine"; 12 | 13 | @RootCommand() 14 | class RootCommandHandle implements CommandStruct { 15 | @Option("msg", "m") 16 | public msg = "default value of msg"; 17 | 18 | @Option() 19 | public notice = "default value of notice"; 20 | 21 | @VariadicOption({ alias: "p" }) 22 | public projects: string[] = []; 23 | 24 | @Input("description of inputs") 25 | public these_are_inputs: string; 26 | 27 | @Options() 28 | public options: unknown; 29 | 30 | public run(): void {} 31 | } 32 | 33 | @Command("update", "u", "update command") 34 | class UpdateCommandHandle implements CommandStruct { 35 | @Option("msg", "m") 36 | public msg = "default value of msg"; 37 | 38 | @Option() 39 | public notice = "default value of notice"; 40 | 41 | @VariadicOption({ alias: "p" }) 42 | public projects: string[] = []; 43 | 44 | @Input("description of inputs") 45 | public these_are_inputs: string; 46 | 47 | @Options() 48 | public options: unknown; 49 | 50 | public run(): void {} 51 | } 52 | 53 | @Command("sync", "s", "sync command") 54 | class SyncCommandHandle implements CommandStruct { 55 | @Option("msg", "m") 56 | public msg = "default value of msg"; 57 | 58 | @Option() 59 | public notice = "default value of notice"; 60 | 61 | @VariadicOption({ alias: "p" }) 62 | public projects: string[] = []; 63 | 64 | @Input("description of inputs") 65 | public these_are_inputs: string; 66 | 67 | @Options() 68 | public options: unknown; 69 | 70 | public run(): void {} 71 | } 72 | 73 | @App({ 74 | name: "mm", 75 | commands: [RootCommandHandle, UpdateCommandHandle, SyncCommandHandle], 76 | }) 77 | class Project {} 78 | 79 | MustardFactory.init(Project).start(); 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .TODO 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Microbundle cache 55 | .rpt2_cache/ 56 | .rts2_cache_cjs/ 57 | .rts2_cache_es/ 58 | .rts2_cache_umd/ 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # Next.js build output 77 | .next 78 | 79 | # Nuxt.js build / generate output 80 | .nuxt 81 | dist 82 | 83 | # Gatsby files 84 | .cache/ 85 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 86 | # https://nextjs.org/blog/next-9-1#public-directory-support 87 | # public 88 | 89 | # vuepress build output 90 | .vuepress/dist 91 | 92 | # Serverless directories 93 | .serverless/ 94 | 95 | # FuseBox cache 96 | .fusebox/ 97 | 98 | # DynamoDB Local files 99 | .dynamodb/ 100 | 101 | # TernJS port file 102 | .tern-port 103 | -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/4.Configurations.md: -------------------------------------------------------------------------------- 1 | # App Configurations 2 | 3 | You can also provide global-level configuration for Mustard. 4 | 5 | - `allowUnknownOptions`: Allow unknown options to be passed to the command. If this is set to `false`, Mustard will throw an error when it encounters an unknown option during parse stage. 6 | - `enableUsage`: Allow Mustard to generate usage information for commands when `--help` or `-h` option is passed. You can also specify a custom usage generator: 7 | 8 | ```typescript 9 | @App({ 10 | name: "my-mustard-app", 11 | configurations: { 12 | enableUsage: (command: CommandRegistryPayload) => `Help info for ${command.commandInvokeName}.` 13 | }, 14 | }) 15 | class Project implements MustardApp { 16 | onStart() {} 17 | 18 | onComplete() {} 19 | } 20 | ``` 21 | 22 | - `enableVersion`: Allow Mustard to generate version information for commands when `--version` or `-v` option is passed. You can also specify a custom version string. 23 | 24 | ```typescript 25 | @App({ 26 | name: "my-mustard-app", 27 | configurations: { 28 | enableVersion: require(path.resolve("./package.json")).version, 29 | }, 30 | }) 31 | class Project implements MustardApp { } 32 | ``` 33 | 34 | - `ignoreValidationErrors`: Ignore validation errors when parsing options. If this is set to `false`, Mustard will throw an error when it encounters a validation error during parse stage. 35 | 36 | - `lifeCycles`: Specify the life cycle of the command. Mustard will execute the corresponding life cycle function when the command is executed. You can also specify a custom life cycle function. 37 | 38 | ```typescript 39 | @App({ 40 | name: "my-mustard-app", 41 | configurations: { 42 | lifeCycles: { 43 | onStart() {} 44 | onError(error) {} 45 | onComplete() {} 46 | } 47 | }, 48 | }) 49 | class Project implements MustardApp { } 50 | ``` 51 | 52 | This is equivalent to: 53 | 54 | ```typescript 55 | @App({ 56 | name: "my-mustard-app", 57 | configurations: { }, 58 | }) 59 | class Project implements MustardApp { 60 | onStart() {} 61 | onError(error) {} 62 | onComplete() {} 63 | } 64 | ``` -------------------------------------------------------------------------------- /packages/create-mustard-app/template-advanced/src/index.mts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module"; 2 | 3 | import { MustardFactory, MustardUtils } from "mustard-cli"; 4 | import { 5 | Command, 6 | Option, 7 | VariadicOption, 8 | App, 9 | Utils, 10 | Input, 11 | } from "mustard-cli/decorator"; 12 | import type { CommandStruct, MustardApp } from "mustard-cli/cli"; 13 | 14 | const require = createRequire(import.meta.url); // construct the require method 15 | 16 | @Command("run", "r") 17 | class RunCommandHandle implements CommandStruct { 18 | @Option("dry", "d", "dry run command to see what will happen") 19 | public dry = false; 20 | 21 | @VariadicOption("projects", "p", "projects to run") 22 | public projects: string[] = []; 23 | 24 | @Utils() 25 | public utils!: MustardUtils; 26 | 27 | @Input() 28 | public input: string = "default"; 29 | 30 | public run(): void { 31 | console.log( 32 | "awesome-mustard-app run command", 33 | this.utils.colors.white(`v ${require("../package.json").version}\n`) 34 | ); 35 | console.log(`Input: ${this.input}`); 36 | console.log(`Run Projects: ${this.input}`); 37 | } 38 | } 39 | 40 | @Command("update", "u") 41 | class UpdateCommandHandle implements CommandStruct { 42 | @Option("dry", "d", "dry run command to see what will happen") 43 | public dry = false; 44 | 45 | @VariadicOption("packages", "p", "packages to update") 46 | public packages: string[] = []; 47 | 48 | @Utils() 49 | public utils!: MustardUtils; 50 | 51 | @Input() 52 | public input: string = "default"; 53 | 54 | public run(): void { 55 | console.log( 56 | "awesome-mustard-app update command", 57 | this.utils.colors.white(`v ${require("../package.json").version}\n`) 58 | ); 59 | console.log(`Input: ${this.input}`); 60 | console.log(`Update Packages: ${this.input}`); 61 | } 62 | } 63 | 64 | @App({ 65 | name: "awesome-mustard-app", 66 | commands: [RunCommandHandle, UpdateCommandHandle], 67 | }) 68 | class Project implements MustardApp { 69 | onError(error: Error): void { 70 | console.log(error); 71 | } 72 | } 73 | 74 | MustardFactory.init(Project).start(); 75 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | # Install and cache deps, build project, run unit testing and integration testing 2 | name: Workflow 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | branches: 9 | - main 10 | - "feat/**" 11 | - "fix/**" 12 | - "chore/**" 13 | - "release/**" 14 | 15 | jobs: 16 | complete: 17 | name: Complete Workflow 18 | 19 | strategy: 20 | matrix: 21 | node-version: [16, 18] 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | 24 | runs-on: ${{ matrix.os }} 25 | 26 | defaults: 27 | run: 28 | working-directory: ./ 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | # cache: "npm" 38 | # cache-dependency-path: packages/mustard-cli/package.json 39 | 40 | - name: Setup pnpm 41 | uses: pnpm/action-setup@v2.2.4 42 | with: 43 | version: latest 44 | run_install: false 45 | 46 | - name: Get pnpm store directory 47 | id: pnpm-cache 48 | shell: bash 49 | run: | 50 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 51 | 52 | - uses: actions/cache@v3 53 | name: Setup pnpm cache 54 | with: 55 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 56 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 57 | restore-keys: | 58 | ${{ runner.os }}-pnpm-store- 59 | 60 | - name: Install dependencies 61 | run: pnpm install 62 | 63 | - name: Build project 64 | run: pnpm --filter mustard-cli run build 65 | 66 | - name: Run unit testing 67 | run: pnpm --filter mustard-cli run test:unit 68 | 69 | # - name: Run integration testing 70 | # run: pnpm --filter mustard-cli run test:ig 71 | 72 | - name: Run publint 73 | run: pnpm --filter mustard-cli pub:check 74 | 75 | - name: Coverage report 76 | if: runner.os != 'Windows' 77 | uses: codecov/codecov-action@v3 78 | with: 79 | directory: ./packages/mustard-cli/coverage 80 | verbose: true 81 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/Registry.ts: -------------------------------------------------------------------------------- 1 | import { MustardConstanst } from "./Constants"; 2 | 3 | import type { CommandRegistryPayload } from "../Typings/Command.struct"; 4 | import type { Dictionary } from "../Typings/Shared.struct"; 5 | 6 | const CommandRegistry = Map>; 7 | 8 | export class MustardRegistry { 9 | private static InitCommandRegistry = new CommandRegistry(); 10 | 11 | private static CommandRegistry = new CommandRegistry(); 12 | 13 | public static registerInit( 14 | identifier: string, 15 | payload: Partial 16 | ) { 17 | MustardRegistry.InitCommandRegistry.set(identifier, payload); 18 | } 19 | 20 | public static register( 21 | identifier: string, 22 | payload: Partial 23 | ) { 24 | MustardRegistry.CommandRegistry.set(identifier, payload); 25 | } 26 | 27 | public static upsert( 28 | identifier: string, 29 | payload: Partial 30 | ) { 31 | const prev = MustardRegistry.provide(identifier); 32 | 33 | if (prev) { 34 | MustardRegistry.register(identifier, { 35 | ...prev, 36 | ...payload, 37 | }); 38 | } else { 39 | MustardRegistry.register(identifier, payload); 40 | } 41 | } 42 | 43 | public static provideInit(): Map; 44 | public static provideInit(identifier: string): CommandRegistryPayload; 45 | public static provideInit(identifier?: string) { 46 | return identifier 47 | ? MustardRegistry.InitCommandRegistry.get(identifier) 48 | : MustardRegistry.InitCommandRegistry; 49 | } 50 | 51 | public static provide(): Map; 52 | public static provide(identifier: string): CommandRegistryPayload; 53 | public static provide(identifier?: string) { 54 | return identifier 55 | ? MustardRegistry.CommandRegistry.get(identifier) 56 | : MustardRegistry.CommandRegistry; 57 | } 58 | 59 | public static provideRootCommand(): CommandRegistryPayload { 60 | return MustardRegistry.provide(MustardConstanst.RootCommandRegistryKey); 61 | } 62 | 63 | public static VariadicOptions = new Set(); 64 | 65 | // raw - alias 66 | public static OptionAliasMap: Dictionary = {}; 67 | 68 | public static ExternalProviderRegistry = new Map(); 69 | } 70 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Errors.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | 3 | import { CommandNotFoundError } from "../Errors/CommandNotFoundError"; 4 | import { MultiRootCommandError } from "../Errors/MultiRootCommandError"; 5 | import { NoRootHandlerError } from "../Errors/NoRootHandlerError"; 6 | import { NullishFactoryOptionError } from "../Errors/NullishFactoryOptionError"; 7 | import { UnknownOptionsError } from "../Errors/UnknownOptionsError"; 8 | import { ValidationError } from "../Errors/ValidationError"; 9 | 10 | describe("Mustard Errors", () => { 11 | it("should produce CommandNotFoundError", () => { 12 | const error = new CommandNotFoundError({ _: [] }); 13 | expect(error.name).toBe("CommandNotFoundError"); 14 | expect(error.message).toMatchInlineSnapshot(` 15 | "Command not found with parsed args: { 16 | \\"_\\": [] 17 | }" 18 | `); 19 | }); 20 | 21 | it("should produce MultiRootCommandError", () => { 22 | class Foo {} 23 | 24 | class Bar {} 25 | const error = new MultiRootCommandError(Foo, Bar); 26 | expect(error.name).toBe("MultiRootCommandError"); 27 | expect(error.message).toMatchInlineSnapshot( 28 | '"Multiple root command detected, RootCommand Foo was already registered, and now Bar is also registered as root command"' 29 | ); 30 | }); 31 | 32 | it("should produce NoRootHandlerError", () => { 33 | const error = new NoRootHandlerError(); 34 | expect(error.name).toBe("NoRootHandlerError"); 35 | expect(error.message).toMatchInlineSnapshot( 36 | "\"No root handler found, please provide command decorated with '@RootCommand' or enable option enableUsage for usage info generation.\"" 37 | ); 38 | }); 39 | 40 | it("should produce NullishFactoryOptionError", () => { 41 | const error = new NullishFactoryOptionError(); 42 | expect(error.name).toBe("NullishFactoryOptionError"); 43 | expect(error.message).toMatchInlineSnapshot( 44 | '"Mustard factory option not initialized, use @App to initialize entry class"' 45 | ); 46 | }); 47 | 48 | it("should produce UnknownOptionsError", () => { 49 | const error = new UnknownOptionsError(["foo", "bar"]); 50 | expect(error.name).toBe("UnknownOptionsError"); 51 | expect(error.message).toMatchInlineSnapshot( 52 | '"Unknown options: foo, bar. See --help for usage."' 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/RestrictValue/RestrictValue.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { execaCommand } from "execa"; 3 | import { TestHelper } from "../../Fixtures/TestHelper"; 4 | import path from "path"; 5 | 6 | const UsagePath = path.resolve(__dirname, "./Usage.ts"); 7 | 8 | describe("IntegrationTesting:RestrictedValues", () => { 9 | it("should apply restriction", async () => { 10 | const { stdout: stdoutWithRoot1 } = await execaCommand( 11 | `${TestHelper.IntegrationExecutor} ${UsagePath}` 12 | ); 13 | expect(stdoutWithRoot1).toMatchInlineSnapshot(` 14 | "Root Command 15 | --notRestrict option: default value of notRestrict 16 | --restrictedArrayTypeOption option: foo 17 | --restrictedObjectTypeOption option: foo 18 | --restrictedEnumTypeOption option: foo" 19 | `); 20 | 21 | const { stdout: stdoutWithRoot2 } = await execaCommand( 22 | `${TestHelper.IntegrationExecutor} ${UsagePath} --notRestrict foo --restrictedArrayTypeOption foo --restrictedObjectTypeOption foo --restrictedEnumTypeOption foo` 23 | ); 24 | expect(stdoutWithRoot2).toMatchInlineSnapshot(` 25 | "Root Command 26 | --notRestrict option: foo 27 | --restrictedArrayTypeOption option: foo 28 | --restrictedObjectTypeOption option: foo 29 | --restrictedEnumTypeOption option: foo" 30 | `); 31 | 32 | const { stdout: stdoutWithRoot3 } = await execaCommand( 33 | `${TestHelper.IntegrationExecutor} ${UsagePath} --notRestrict foo --restrictedArrayTypeOption foo --restrictedObjectTypeOption foo --restrictedEnumTypeOption foo` 34 | ); 35 | expect(stdoutWithRoot3).toMatchInlineSnapshot(` 36 | "Root Command 37 | --notRestrict option: foo 38 | --restrictedArrayTypeOption option: foo 39 | --restrictedObjectTypeOption option: foo 40 | --restrictedEnumTypeOption option: foo" 41 | `); 42 | 43 | const { stdout: stdoutWithRoot4 } = await execaCommand( 44 | `${TestHelper.IntegrationExecutor} ${UsagePath} --notRestrict qux --restrictedArrayTypeOption qux --restrictedObjectTypeOption qux --restrictedEnumTypeOption qux` 45 | ); 46 | expect(stdoutWithRoot4).toMatchInlineSnapshot(` 47 | "Root Command 48 | --notRestrict option: qux 49 | --restrictedArrayTypeOption option: foo 50 | --restrictedObjectTypeOption option: foo 51 | --restrictedEnumTypeOption option: foo" 52 | `); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/Root/Root.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { execaCommand } from "execa"; 3 | import { TestHelper } from "../../Fixtures/TestHelper"; 4 | import path from "path"; 5 | 6 | const UsagePath1 = path.resolve(__dirname, "./Usage.ts"); 7 | 8 | describe("IntegrationTesting:RootCommandHandle", () => { 9 | it("should use root command as handler", async () => { 10 | const { stdout: stdout1 } = await execaCommand( 11 | `${TestHelper.IntegrationExecutor} ${UsagePath1}` 12 | ); 13 | expect(stdout1).toMatchInlineSnapshot( 14 | ` 15 | "--msg option: default value of msg 16 | --projects option: 17 | inputs: 18 | options: {\\"msg\\":\\"default value of msg\\",\\"projects\\":[]}" 19 | ` 20 | ); 21 | 22 | const { stdout: stdout2 } = await execaCommand( 23 | `${TestHelper.IntegrationExecutor} ${UsagePath1} --msg Hello` 24 | ); 25 | expect(stdout2).toMatchInlineSnapshot( 26 | ` 27 | "--msg option: Hello 28 | --projects option: 29 | inputs: 30 | options: {\\"msg\\":\\"Hello\\",\\"projects\\":[]}" 31 | ` 32 | ); 33 | 34 | const { stdout: stdout3 } = await execaCommand( 35 | `${TestHelper.IntegrationExecutor} ${UsagePath1} --msg Hello --projects app1 app2 app3` 36 | ); 37 | expect(stdout3).toMatchInlineSnapshot( 38 | ` 39 | "--msg option: Hello 40 | --projects option: app1,app2,app3 41 | inputs: 42 | options: {\\"msg\\":\\"Hello\\",\\"projects\\":[\\"app1\\",\\"app2\\",\\"app3\\"]}" 43 | ` 44 | ); 45 | 46 | const { stdout: stdout4 } = await execaCommand( 47 | `${TestHelper.IntegrationExecutor} ${UsagePath1} enhance --msg Hello --projects app1 app2 app3` 48 | ); 49 | expect(stdout4).toMatchInlineSnapshot( 50 | ` 51 | "--msg option: Hello 52 | --projects option: app1,app2,app3 53 | inputs: enhance 54 | options: {\\"msg\\":\\"Hello\\",\\"projects\\":[\\"app1\\",\\"app2\\",\\"app3\\"]}" 55 | ` 56 | ); 57 | 58 | const { stdout: stdout5 } = await execaCommand( 59 | `${TestHelper.IntegrationExecutor} ${UsagePath1} enhance -m Hello -p app1 app2 app3` 60 | ); 61 | expect(stdout5).toMatchInlineSnapshot( 62 | ` 63 | "--msg option: Hello 64 | --projects option: app1,app2,app3 65 | inputs: enhance 66 | options: {\\"msg\\":\\"Hello\\",\\"projects\\":[]}" 67 | ` 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/MultiRootCommands.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { execaCommand } from "execa"; 3 | import { TestHelper } from "../../Fixtures/TestHelper"; 4 | import path from "path"; 5 | 6 | const UsagePath = path.resolve(__dirname, "./Usage.ts"); 7 | 8 | describe("IntegrationTesting:MultiRootCommands", () => { 9 | it.skip("should throw error", async () => { 10 | const { stderr } = await execaCommand( 11 | `${TestHelper.IntegrationExecutor} ${UsagePath}`, 12 | { 13 | reject: false, 14 | } 15 | ); 16 | expect(stderr).toMatchInlineSnapshot( 17 | ` 18 | "/Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts:13 19 | @Option() 20 | ^ 21 | MultiRootCommandError: Multiple root command detected, RootCommand RootCommandHandle1 was already registered, and now RootCommandHandle2 is also registered as root command 22 | at /Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/Decorators/Command.ts:218:15 23 | at __esDecorate (/Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts:13:40) 24 | at /Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts:38:1 25 | at /Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts:130:7 26 | at Object. (/Users/linbudu/Desktop/OPEN_SOURCE/Mustard/packages/mustard-cli/source/__tests__/Integrations/MultiRootCommands/Usage.ts:132:3) 27 | at Module._compile (node:internal/modules/cjs/loader:1095:14) 28 | at Module.m._compile (/Users/linbudu/Desktop/OPEN_SOURCE/Mustard/node_modules/.pnpm/ts-node@10.9.1_3wpmmxphde7mszzmvhplwryuka/node_modules/ts-node/src/index.ts:1618:23) 29 | at Module._extensions..js (node:internal/modules/cjs/loader:1124:10) 30 | at Object.require.extensions. [as .ts] (/Users/linbudu/Desktop/OPEN_SOURCE/Mustard/node_modules/.pnpm/ts-node@10.9.1_3wpmmxphde7mszzmvhplwryuka/node_modules/ts-node/src/index.ts:1621:12) 31 | at Module.load (node:internal/modules/cjs/loader:975:32) { 32 | existClass: [class RootCommandHandle1], 33 | incomingClass: [class RootCommandHandle2] 34 | }" 35 | ` 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/sample/index.mts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module"; 2 | 3 | import { MustardFactory } from "mustard-cli"; 4 | import { 5 | Command, 6 | RootCommand, 7 | Option, 8 | VariadicOption, 9 | App, 10 | Input, 11 | } from "mustard-cli/decorator"; 12 | import { Validator } from "mustard-cli/validator"; 13 | import type { CommandStruct, MustardApp } from "mustard-cli/cli"; 14 | 15 | import path from "path"; 16 | 17 | const require = createRequire(import.meta.url); 18 | 19 | @RootCommand() 20 | class RootCommandHandle implements CommandStruct { 21 | @Option("msg", "m", Validator.Required().String().MinLength(5)) 22 | public msg = "default value of msg"; 23 | 24 | public run(): void { 25 | console.log(`Root command executed with: msg: ${this.msg}`); 26 | } 27 | } 28 | 29 | @Command("update", "u", "update project dependencies") 30 | class UpdateCommand implements CommandStruct { 31 | @Option("depth", "depth of packages to update", Validator.Number().Gte(1)) 32 | public depth = 10; 33 | 34 | @Option(Validator.Boolean()) 35 | public dry = false; 36 | 37 | @Option({ name: "target", alias: "t" }) 38 | public targetOption: string; 39 | 40 | @Input() 41 | public input: string[] = []; 42 | 43 | @VariadicOption() 44 | public packages: string[] = []; 45 | 46 | public run(): void { 47 | console.log( 48 | `Update command executed with: depth: ${this.depth}, dry: ${ 49 | this.dry 50 | }, targetOption: ${this.targetOption}, input: ${JSON.stringify( 51 | this.input 52 | )}, packages: ${JSON.stringify(this.packages)}` 53 | ); 54 | } 55 | } 56 | 57 | @Command("sync", "s", "sync project") 58 | class SyncCommand implements CommandStruct { 59 | @Option("depth", "depth of packages to update", Validator.Number().Gte(1)) 60 | public depth = 10; 61 | 62 | @Option(Validator.Boolean()) 63 | public dry = false; 64 | 65 | @Option({ name: "target", alias: "t" }) 66 | public targetOption: string; 67 | 68 | @Input() 69 | public input: string[] = []; 70 | 71 | @VariadicOption() 72 | public packages: string[] = []; 73 | 74 | public run(): void {} 75 | } 76 | 77 | @App({ 78 | name: "create-mustard-app", 79 | commands: [RootCommandHandle, UpdateCommand, SyncCommand], 80 | configurations: { 81 | allowUnknownOptions: true, 82 | enableUsage: true, 83 | enableVersion: require(path.resolve("./package.json")).version, 84 | }, 85 | }) 86 | class Project implements MustardApp { 87 | onStart() {} 88 | 89 | onComplete() {} 90 | } 91 | 92 | MustardFactory.init(Project).start(); 93 | -------------------------------------------------------------------------------- /packages/create-mustard-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-mustard-app 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - bump typescript version to 5.3.3 8 | - Updated dependencies 9 | - mustard-cli@1.1.1 10 | 11 | ## 1.1.0 12 | 13 | ### Minor Changes 14 | 15 | - bump typescript version 16 | 17 | ### Patch Changes 18 | 19 | - Updated dependencies 20 | - mustard-cli@1.1.0 21 | 22 | ## 1.0.0 23 | 24 | ### Major Changes 25 | 26 | - 292778d: 1.0.0 release 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [292778d] 31 | - mustard-cli@1.0.0 32 | 33 | ## 0.4.3 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies [d6aca87] 38 | - Updated dependencies [fdf73a3] 39 | - mustard-cli@0.8.3 40 | 41 | ## 0.4.2 42 | 43 | ### Patch Changes 44 | 45 | - bump ts version 46 | - Updated dependencies [0479502] 47 | - Updated dependencies 48 | - mustard-cli@0.8.2 49 | 50 | ## 0.4.1 51 | 52 | ### Patch Changes 53 | 54 | - Provide package-specified README 55 | - Updated dependencies 56 | - mustard-cli@0.8.1 57 | 58 | ## 0.4.0 59 | 60 | ### Minor Changes 61 | 62 | - fix template dev workflow 63 | 64 | ### Patch Changes 65 | 66 | - Updated dependencies 67 | - Updated dependencies 68 | - mustard-cli@0.8.0 69 | 70 | ## 0.3.1 71 | 72 | ### Patch Changes 73 | 74 | - fixup package import path 75 | - Updated dependencies 76 | - mustard-cli@0.7.1 77 | 78 | ## 0.3.0 79 | 80 | ### Minor Changes 81 | 82 | - 036f9b9: Fix starter dependencies 83 | 84 | ### Patch Changes 85 | 86 | - 477db3f: Remove deps from templates 87 | - Updated dependencies [036f9b9] 88 | - mustard-cli@0.7.0 89 | 90 | ## 0.2.3 91 | 92 | ### Patch Changes 93 | 94 | - fixup \_\_dirname in es scope 95 | - 746ae49: Fix build config, use PURE ESM 96 | - Updated dependencies 97 | - Updated dependencies [746ae49] 98 | - mustard-cli@0.6.3 99 | 100 | ## 0.2.0 101 | 102 | ### Minor Changes 103 | 104 | - use module:es2022 105 | 106 | ### Patch Changes 107 | 108 | - Updated dependencies 109 | - mustard-cli@0.6.0 110 | 111 | ## 0.1.0 112 | 113 | ### Minor Changes 114 | 115 | - initial release version 116 | - 0d1aa23: initial implementation 117 | 118 | ## 0.0.2 119 | 120 | ### Patch Changes 121 | 122 | - Bump typescript version to 5.0.0 beta 123 | - 7c32d55: Fix UsageGenerator for all suits 124 | - 7302020: Configuration and Usage info collection for @Input 125 | - Updated dependencies [4902a30] 126 | - Updated dependencies 127 | - Updated dependencies [7c32d55] 128 | - Updated dependencies [7302020] 129 | - mustard-cli@0.5.0 130 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Commands/BuiltInCommands.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | import { UsageInfoGenerator } from "../Components/UsageGenerator"; 4 | import { MustardConstanst } from "../Components/Constants"; 5 | 6 | import type { Configurations } from "../Typings/Configuration.struct"; 7 | import type { Arguments } from "yargs-parser"; 8 | import type { CommandRegistryPayload } from "../Typings/Command.struct"; 9 | import type { MaybeFactory } from "../Typings/Shared.struct"; 10 | 11 | export class BuiltInCommands { 12 | public static containsHelpFlag(parsedArgs: Arguments): boolean { 13 | return Boolean( 14 | parsedArgs["help"] || 15 | parsedArgs["h"] || 16 | parsedArgs[MustardConstanst.InternalHelpFlag] 17 | ); 18 | } 19 | 20 | public static containsVersionFlag(parsedArgs: Arguments): boolean { 21 | return Boolean( 22 | parsedArgs["version"] || 23 | parsedArgs["v"] || 24 | parsedArgs[MustardConstanst.InternalVersionFlag] 25 | ); 26 | } 27 | 28 | private static useController( 29 | factory: MaybeFactory, 30 | ...factoryArguments: unknown[] 31 | ): T { 32 | return typeof factory === "function" 33 | ? factory(...factoryArguments) 34 | : factory; 35 | } 36 | 37 | public static useHelpCommand( 38 | bin: string, 39 | parsedArgs: Arguments | boolean, 40 | registration?: CommandRegistryPayload, 41 | controller?: Configurations["enableUsage"], 42 | exit = true 43 | ) { 44 | const printHelp = 45 | typeof parsedArgs === "boolean" 46 | ? parsedArgs 47 | : // in ubuntu-latest image, yargs-parser returns undefined for parsedArgs instead of an empty object 48 | BuiltInCommands.containsHelpFlag(parsedArgs ?? {}); 49 | 50 | if (!printHelp) { 51 | return; 52 | } 53 | 54 | UsageInfoGenerator.initGenerator({ 55 | bin, 56 | parsedInputs: typeof parsedArgs === "boolean" ? [] : parsedArgs["_"], 57 | }); 58 | 59 | controller 60 | ? typeof controller === "function" 61 | ? console.log(controller(registration)) 62 | : UsageInfoGenerator.printHelp(registration) 63 | : UsageInfoGenerator.printHelp(registration); 64 | 65 | exit && process.exit(0); 66 | } 67 | 68 | public static useVersionCommand( 69 | parsedArgs: Arguments | boolean, 70 | controller?: Configurations["enableVersion"], 71 | exit = true 72 | ) { 73 | const printVersion = 74 | typeof parsedArgs === "boolean" 75 | ? parsedArgs 76 | : BuiltInCommands.containsVersionFlag(parsedArgs); 77 | 78 | if (!printVersion) { 79 | return; 80 | } 81 | 82 | if (!controller) return; 83 | 84 | console.log(`V ${chalk.bold(BuiltInCommands.useController(controller))}`); 85 | 86 | exit && process.exit(0); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/UsageGenerator/RootAndNestedCommandsProvided.usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | RootCommand, 4 | Option, 5 | VariadicOption, 6 | App, 7 | Input, 8 | Options, 9 | Command, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct } from "../../../Exports/ComanndLine"; 12 | 13 | @RootCommand() 14 | class RootCommandHandle implements CommandStruct { 15 | @Option("msg", "m") 16 | public msg = "default value of msg"; 17 | 18 | @Option() 19 | public notice = "default value of notice"; 20 | 21 | @VariadicOption({ alias: "p" }) 22 | public projects: string[] = []; 23 | 24 | @Input("description of inputs") 25 | public these_are_inputs: string; 26 | 27 | @Options() 28 | public options: unknown; 29 | 30 | public run(): void {} 31 | } 32 | 33 | @Command("account", "a", "update account commandxxx") 34 | class UpdateAccountCommandHandle implements CommandStruct { 35 | @Option("msg", "m") 36 | public msg = "default value of msg"; 37 | 38 | @Option() 39 | public notice = "default value of notice"; 40 | 41 | @VariadicOption({ alias: "p" }) 42 | public projects: string[] = []; 43 | 44 | @Input("description of inputs") 45 | public these_are_inputs: string; 46 | 47 | @Options() 48 | public options: unknown; 49 | 50 | public run(): void {} 51 | } 52 | 53 | @Command("sys", "s", "update sys command") 54 | class UpdateSysCommandHandle implements CommandStruct { 55 | @Option("msg", "m") 56 | public msg = "default value of msg"; 57 | 58 | @Option() 59 | public notice = "default value of notice"; 60 | 61 | @VariadicOption({ alias: "p" }) 62 | public projects: string[] = []; 63 | 64 | @Input("description of inputs") 65 | public these_are_inputs: string; 66 | 67 | @Options() 68 | public options: unknown; 69 | 70 | public run(): void {} 71 | } 72 | 73 | @Command("update", "u", "update command", [ 74 | UpdateAccountCommandHandle, 75 | UpdateSysCommandHandle, 76 | ]) 77 | class UpdateCommandHandle implements CommandStruct { 78 | @Option("msg", "m") 79 | public msg = "default value of msg"; 80 | 81 | @Option() 82 | public notice = "default value of notice"; 83 | 84 | @VariadicOption({ alias: "p", description: "description of projects" }) 85 | public projects: string[] = []; 86 | 87 | @Input("description of inputs") 88 | public these_are_inputs: string; 89 | 90 | @Options() 91 | public options: unknown; 92 | 93 | public run(): void {} 94 | } 95 | 96 | @App({ 97 | name: "mm", 98 | commands: [ 99 | RootCommandHandle, 100 | UpdateCommandHandle, 101 | UpdateAccountCommandHandle, 102 | UpdateSysCommandHandle, 103 | ], 104 | }) 105 | class Project {} 106 | 107 | MustardFactory.init(Project).start(); 108 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/BuiltInCommands/BuiltInCommands.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { execaCommand } from "execa"; 3 | import path from "path"; 4 | import { TestHelper } from "../../Fixtures/TestHelper"; 5 | 6 | const UsagePath1 = path.resolve(__dirname, "./Usage1.ts"); 7 | const UsagePath2 = path.resolve(__dirname, "./Usage2.ts"); 8 | const UsagePath3 = path.resolve(__dirname, "./Usage3.ts"); 9 | 10 | describe("IntegrationTesting:BuiltInCommandsHandle", () => { 11 | it("Usage 1", async () => { 12 | const { stdout: stdout1 } = await execaCommand( 13 | `${TestHelper.IntegrationExecutor} ${UsagePath1} -v` 14 | ); 15 | expect(stdout1).toMatchInlineSnapshot('"V 10.11.0"'); 16 | 17 | const { stdout: stdout2 } = await execaCommand( 18 | `${TestHelper.IntegrationExecutor} ${UsagePath1} --version` 19 | ); 20 | expect(stdout2).toMatchInlineSnapshot('"V 10.11.0"'); 21 | 22 | const { stdout: stdout3 } = await execaCommand( 23 | `${TestHelper.IntegrationExecutor} ${UsagePath1} -h` 24 | ); 25 | expect(stdout3).toMatchInlineSnapshot( 26 | ` 27 | " 28 | Usage: 29 | 30 | $ create-mustard-app 31 | 32 | Options: 33 | " 34 | ` 35 | ); 36 | 37 | const { stdout: stdout4 } = await execaCommand( 38 | `${TestHelper.IntegrationExecutor} ${UsagePath1} --help` 39 | ); 40 | expect(stdout4).toMatchInlineSnapshot( 41 | ` 42 | " 43 | Usage: 44 | 45 | $ create-mustard-app 46 | 47 | Options: 48 | " 49 | ` 50 | ); 51 | }); 52 | 53 | it("Usage 2", async () => { 54 | const { stdout: stdout1 } = await execaCommand( 55 | `${TestHelper.IntegrationExecutor} ${UsagePath2} --help` 56 | ); 57 | expect(stdout1).toMatchInlineSnapshot('"Usage: root"'); 58 | }); 59 | 60 | it("Usage 3", async () => { 61 | const { stdout: stdout2 } = await execaCommand( 62 | `${TestHelper.IntegrationExecutor} ${UsagePath3} update --help` 63 | ); 64 | expect(stdout2).toMatchInlineSnapshot( 65 | ` 66 | " 67 | Usage: 68 | 69 | $ LinbuduLab CLI update [options] 70 | 71 | Command: 72 | update execute update command 73 | 74 | Options: 75 | --name, name of the project 76 | --version, version of the project 77 | " 78 | ` 79 | ); 80 | 81 | const { stdout: stdout3 } = await execaCommand( 82 | `${TestHelper.IntegrationExecutor} ${UsagePath3} sync --help` 83 | ); 84 | expect(stdout3).toMatchInlineSnapshot( 85 | ` 86 | " 87 | Usage: 88 | 89 | $ LinbuduLab CLI sync [options] 90 | 91 | Command: 92 | sync execute update command 93 | 94 | Options: 95 | --name, -n, name of the project 96 | --type, -t, type of the project 97 | " 98 | ` 99 | ); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/create-mustard-app/source/index.mts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "module"; 2 | import fs from "fs-extra"; 3 | import path from "path"; 4 | import { fileURLToPath } from "url"; 5 | import logSymbols from "log-symbols"; 6 | 7 | import { MustardFactory, MustardUtils } from "mustard-cli"; 8 | 9 | import { RootCommand, Option, App, Utils, Input } from "mustard-cli/decorator"; 10 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 11 | 12 | const require = createRequire(import.meta.url); 13 | 14 | const GitIgnoreContent = ` 15 | .vscode 16 | .idea 17 | .DS_Store 18 | node_modules 19 | dist 20 | 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | pnpm-debug.log* 26 | `; 27 | 28 | type Template = "simple" | "complete"; 29 | 30 | @RootCommand() 31 | class RootCommandHandle implements CommandStruct { 32 | @Option("dry", "d", "dry run command to see what will happen") 33 | public dry = false; 34 | 35 | @Option("template", "t", "template to use, 'simple' or 'complete'") 36 | public template: Template = "simple"; 37 | 38 | @Input("directory to create the project in") 39 | public dir: string = "./mustard-app"; 40 | 41 | @Utils() 42 | public utils!: MustardUtils; 43 | 44 | public run(): void { 45 | console.log( 46 | logSymbols.info, 47 | "create-mustard-app", 48 | this.utils.colors.white(`v ${require("../package.json").version}\n`) 49 | ); 50 | 51 | const createDir = path.resolve(this.dir); 52 | 53 | console.log( 54 | logSymbols.info, 55 | `Creating project in ${this.utils.colors.white(createDir)}...` 56 | ); 57 | 58 | const template = ["simple", "complete"].includes(this.template) 59 | ? this.template 60 | : "simple"; 61 | 62 | const templatePath = path.resolve( 63 | path.dirname(fileURLToPath(import.meta.url)), 64 | `../template-${template}` 65 | ); 66 | 67 | const gitIgnorePath = path.join(createDir, ".gitignore"); 68 | 69 | try { 70 | fs.ensureDirSync(createDir); 71 | fs.copySync(templatePath, createDir, {}); 72 | fs.writeFileSync(gitIgnorePath, GitIgnoreContent); 73 | 74 | console.log( 75 | "\n", 76 | logSymbols.success, 77 | `Project ${this.utils.colors.white( 78 | this.dir 79 | )} created successfully, enjoy!` 80 | ); 81 | } catch (error) { 82 | fs.removeSync(createDir); 83 | console.log( 84 | logSymbols.error, 85 | "Project creation failed wtih following error:" 86 | ); 87 | throw error; 88 | } 89 | } 90 | } 91 | 92 | @App({ 93 | name: "create-mustard-app", 94 | commands: [RootCommandHandle], 95 | configurations: { 96 | allowUnknownOptions: true, 97 | enableUsage: true, 98 | }, 99 | }) 100 | class Project implements MustardApp { 101 | onError(error: Error): void { 102 | console.log(error); 103 | } 104 | } 105 | 106 | MustardFactory.init(Project).start(); 107 | -------------------------------------------------------------------------------- /packages/mustard-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mustard-cli", 3 | "version": "1.1.1", 4 | "homepage": "https://github.com/LinbuduLab/Mustard/tree/main/packages/mustard-cli#readme", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/LinbuduLab/Mustard.git", 8 | "directory": "packages/mustard-cli" 9 | }, 10 | "type": "module", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/Exports/index.d.ts", 14 | "import": "./dist/Exports/index.js", 15 | "require": "./dist/Exports/index.js" 16 | }, 17 | "./decorator": { 18 | "types": "./dist/Exports/Decorators.d.ts", 19 | "import": "./dist/Exports/Decorators.js", 20 | "require": "./dist/Exports/Decorators.js" 21 | }, 22 | "./cli": { 23 | "types": "./dist/Exports/ComanndLine.d.ts", 24 | "import": "./dist/Exports/ComanndLine.js", 25 | "require": "./dist/Exports/ComanndLine.js" 26 | }, 27 | "./validator": { 28 | "types": "./dist/Exports/Validator.d.ts", 29 | "import": "./dist/Exports/Validator.js", 30 | "require": "./dist/Exports/Validator.js" 31 | } 32 | }, 33 | "main": "./dist/Exports/index.js", 34 | "types": "./dist/Exports/index.d.ts", 35 | "typesVersions": { 36 | "*": { 37 | "decorator": [ 38 | "./dist/Exports/Decorators.d.ts" 39 | ], 40 | "cli": [ 41 | "./dist/Exports/ComanndLine.d.ts" 42 | ], 43 | "validator": [ 44 | "./dist/Exports/Validator.d.ts" 45 | ] 46 | } 47 | }, 48 | "scripts": { 49 | "build": "tsc --declarationMap false", 50 | "dev": "tsc --watch", 51 | "local": "nodemon index.ts run sync --dry", 52 | "prepublishOnly": "pnpm run pub:check && pnpm run build", 53 | "pub:check": "pnpx publint", 54 | "test": "NODE_OPTIONS='--experimental-specifier-resolution=node' vitest", 55 | "test:ig": "cross-env TEST_TYPE=INTEGRATION NODE_OPTIONS='--experimental-specifier-resolution=node' vitest", 56 | "test:igu": "cross-env TEST_TYPE=INTEGRATION NODE_OPTIONS='--experimental-specifier-resolution=node' vitest -u", 57 | "test:unit": "cross-env TEST_TYPE=UNIT vitest" 58 | }, 59 | "nodemonConfig": { 60 | "delay": 500, 61 | "env": { 62 | "NODE_ENV": "development" 63 | }, 64 | "execMap": { 65 | "ts": "ts-node-esm" 66 | }, 67 | "ext": "ts,json", 68 | "ignore": [ 69 | "**/test/**", 70 | "**/docs/**", 71 | "node_modules" 72 | ], 73 | "restartable": "rs", 74 | "verbose": true, 75 | "watch": [ 76 | "*.ts" 77 | ] 78 | }, 79 | "dependencies": { 80 | "chalk": "^5.0.0", 81 | "debug": "^4.3.4", 82 | "fastest-levenshtein": "^1.0.16", 83 | "find-up": "^6.3.0", 84 | "lodash.groupby": "^4.6.0", 85 | "lodash.uniqby": "^4.7.0", 86 | "mri": "^1.2.0", 87 | "yargs-parser": "^21.1.1", 88 | "zod": "^3.20.2" 89 | }, 90 | "devDependencies": { 91 | "@types/debug": "^4.1.7", 92 | "@types/lodash.groupby": "^4.6.7", 93 | "@types/lodash.uniqby": "^4.7.7", 94 | "@types/node": "^18.11.17", 95 | "@types/tmp": "^0.2.3", 96 | "@types/yargs-parser": "^21.0.0", 97 | "@vitest/coverage-c8": "^0.29.3", 98 | "cross-env": "^7.0.3", 99 | "execa": "^6.0.0", 100 | "lodash": "^4.17.21", 101 | "nodemon": "^2.0.20", 102 | "tmp": "^0.2.1", 103 | "ts-node": "^10.9.1", 104 | "tsconfig-paths": "^4.1.1", 105 | "typescript": "^5.3.3", 106 | "vitest": "^0.29.3" 107 | }, 108 | "peerDependencies": { 109 | "typescript": ">=5.0.0" 110 | }, 111 | "publishConfig": { 112 | "access": "public", 113 | "registry": "https://registry.npmjs.org/" 114 | } 115 | } -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/5.UsageGenerator.md: -------------------------------------------------------------------------------- 1 | # Usage Generators 2 | 3 | Mustard provides built-in usage generators for commands, which will collect all the options info and command registry info to generate usage information for commands. 4 | 5 | ```typescript 6 | import { MustardFactory } from "../../../Exports/index"; 7 | import { 8 | RootCommand, 9 | Option, 10 | VariadicOption, 11 | App, 12 | Input, 13 | Options, 14 | Command, 15 | } from "../../../Exports/Decorators"; 16 | import { CommandStruct } from "../../../Exports/ComanndLine"; 17 | 18 | @Command("update", "u", "update command") 19 | class UpdateCommandHandle implements CommandStruct { 20 | @Option("msg", "m") 21 | public msg = "default value of msg"; 22 | 23 | @Option() 24 | public notice = "default value of notice"; 25 | 26 | @VariadicOption({ alias: "p" }) 27 | public projects: string[] = []; 28 | 29 | @Input("description of inputs") 30 | public these_are_inputs: string; 31 | 32 | @Options() 33 | public options: unknown; 34 | 35 | public run(): void {} 36 | } 37 | 38 | @App({ 39 | name: "mm", 40 | commands: [UpdateCommandHandle], 41 | }) 42 | class Project {} 43 | 44 | MustardFactory.init(Project).start(); 45 | ``` 46 | 47 | Generated usage information: 48 | 49 | ```shell 50 | Usage: 51 | 52 | $ mm [command] [--options] 53 | 54 | Command: 55 | update, u, update command 56 | 57 | Options: 58 | --msg, -m, default: "default value of msg" 59 | --notice, default: "default value of notice" 60 | --projects, -p, default: [] 61 | ``` 62 | 63 | Nested commands: 64 | 65 | ```typescript 66 | import { MustardFactory } from "../../../Exports/index"; 67 | import { 68 | RootCommand, 69 | Option, 70 | VariadicOption, 71 | App, 72 | Input, 73 | Options, 74 | Command, 75 | } from "../../../Exports/Decorators"; 76 | import { CommandStruct } from "../../../Exports/ComanndLine"; 77 | 78 | @Command("update", "u", "update command") 79 | class UpdateCommandHandle implements CommandStruct { 80 | @Option("msg", "m") 81 | public msg = "default value of msg"; 82 | 83 | @Option() 84 | public notice = "default value of notice"; 85 | 86 | @VariadicOption({ alias: "p" }) 87 | public projects: string[] = []; 88 | 89 | @Input("description of inputs") 90 | public these_are_inputs: string; 91 | 92 | @Options() 93 | public options: unknown; 94 | 95 | public run(): void {} 96 | } 97 | 98 | @Command("sync", "s", "sync command") 99 | class SyncCommandHandle implements CommandStruct { 100 | @Option("msg", "m") 101 | public msg = "default value of msg"; 102 | 103 | @Option() 104 | public notice = "default value of notice"; 105 | 106 | @VariadicOption({ alias: "p" }) 107 | public projects: string[] = []; 108 | 109 | @Input("description of inputs") 110 | public these_are_inputs: string; 111 | 112 | @Options() 113 | public options: unknown; 114 | 115 | public run(): void {} 116 | } 117 | 118 | @App({ 119 | name: "mm", 120 | commands: [UpdateCommandHandle, SyncCommandHandle], 121 | }) 122 | class Project {} 123 | 124 | MustardFactory.init(Project).start(); 125 | ``` 126 | 127 | Generated usage information: 128 | 129 | ```shell 130 | Usage: 131 | 132 | $ mm [command] [--options] 133 | 134 | Command: 135 | update, u, update command 136 | 137 | Options: 138 | --msg, -m, default: "default value of msg" 139 | --notice, default: "default value of notice" 140 | --projects, -p, default: [] 141 | 142 | Command: 143 | sync, s, sync command 144 | 145 | Options: 146 | --msg, -m, default: "default value of msg" 147 | --notice, default: "default value of notice" 148 | --projects, -p, default: [] 149 | ``` -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/0.index.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | ## Register Command 4 | 5 | In command line applications, we usually have two kinds of commands: Root Commands and Sub command. Root commands are invoked with your binary name directly like: 6 | 7 | ```bash 8 | $ create-react-app [dir] [options] 9 | ``` 10 | 11 | And sub commands are invoked with your binary name and sub command name like: 12 | 13 | ```bash 14 | $ vite build 15 | $ parcel build 16 | $ git push 17 | ``` 18 | 19 | The root command and subcommands can co-exist in a command line program and provide more flexible usage. For example, instead of specifying a specific logic for the root command, you can just use it as a prompt for the usage of a subcommand, such as git, or treat the root command as a shortcut to another subcommand, such as `parcel` and `parcel dev`. 20 | 21 | There are no advantages or disadvantages of these two commands, just the need to choose according to the scenario. 22 | 23 | In Mustard, we register root commands with `@RootCommand` decorator, and sub commands with `@Command` decorator. 24 | 25 | ```typescript 26 | import { RootCommand, Command } from "mustard-cli/decorator"; 27 | import type { CommandStruct } from "mustard-cli/cli"; 28 | 29 | @RootCommand() 30 | class RootCommandHandle implements CommandStruct { 31 | public run() {} 32 | } 33 | 34 | @Command("update") 35 | class UpdateCommand implements CommandStruct { 36 | public run() {} 37 | } 38 | ``` 39 | 40 | Root command receives no args, as we cannot specify how it was invoked, but sub command receives configuration including `invoke-name`, `invoke-alias` and `command-description`. 41 | 42 | Also, the command class should implement `CommandStruct` interface, which is a simple interface with only one method `run()`. This method will be invoked when the command got invoked. 43 | 44 | ## Define Command Options 45 | 46 | Another important part of the command line program is the handling of input and options: 47 | 48 | ```bash 49 | bin this-is-input --this-is-option1 foo --this-is-option2 bar 50 | ``` 51 | 52 | In Mustard, we define input with `@Input` decorator, and options with `@Option` decorator. 53 | 54 | ```typescript 55 | import { Option, Input } from "mustard-cli/decorator"; 56 | 57 | @RootCommand() 58 | class RootCommandHandle implements CommandStruct { 59 | @Input() 60 | public input = "default value of input"; 61 | 62 | @Option("message") 63 | public msg = "default value of msg"; 64 | 65 | public run(): void { } 66 | } 67 | ``` 68 | 69 | By applying `@Input` decorator to a property, we can define an input for the command. The input will be parsed as a string and assigned to the property, and when there is no input provided, we can use the default value of property, so the same as `@Option` decorator. 70 | 71 | If you're using variadic option like `--projects p1 p2 p3`, you will need `@VariadicOption` decorator to mark the property as variadic: 72 | 73 | ```typescript 74 | import { VariadicOption } from "mustard-cli/decorator"; 75 | 76 | @RootCommand() 77 | class RootCommandHandle implements CommandStruct { 78 | @VariadicOption() 79 | public projects: string[] = []; 80 | 81 | public run(): void { } 82 | } 83 | ``` 84 | 85 | ## Start Your App! 86 | 87 | After completing the registration of commands and options, you can add these commands to your project and start your application: 88 | 89 | ```typescript 90 | import { MustardFactory } from "mustard-cli"; 91 | import { App } from "mustard-cli/decorator"; 92 | import type { MustardApp } from "mustard-cli/cli"; 93 | 94 | @App({ 95 | name: "my-awesome-app", 96 | commands: [RootCommandHandle, UpdateCommand], 97 | configurations: { }, 98 | }) 99 | class Project implements MustardApp { 100 | onStart() {} 101 | 102 | onComplete() {} 103 | 104 | onError() {} 105 | } 106 | 107 | MustardFactory.init(Project).start(); 108 | ``` 109 | 110 | When the start method is called, Mustard automatically distributes the corresponding command handler based on the info from the command line, injects the input and option information into the command handler. 111 | 112 | You can also do some additional work during execution through the life cycle provided by the `MustardApp` interface. -------------------------------------------------------------------------------- /packages/docs/content/0.intro/0.index.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | Mustard is a command-line application framework based on [the new ES decorator](https://github.com/tc39/proposal-decorators), which aims to provide an **OOP-oriented command-line building paradigm based on the type checking capabilities of native TS**. 4 | 5 | ## Prerequisites 6 | 7 | ::list{type="info"} 8 | - Node.js >= 16.0.0 9 | - TypeScript >= 5.0.0 10 | :: 11 | 12 | ## Create new project 13 | 14 | > By default, this command creates a new project with **only root command**, run with `--template=complete` to create project with commands. 15 | 16 | ::code-group 17 | ```bash [npm] 18 | npx create-mustard-app 19 | ``` 20 | ```bash [yarn] 21 | yarn create-mustard-app 22 | ``` 23 | ```bash [pnpm] 24 | pnpx create-mustard-app 25 | ``` 26 | :: 27 | 28 | ```bash 29 | cd mustard-app 30 | npm run dev 31 | ``` 32 | 33 | ## Start manually 34 | 35 | ::code-group 36 | ```bash [npm] 37 | npm i mustard-cli 38 | npm i typescript ts-node -D 39 | ``` 40 | ```bash [yarn] 41 | yarn add mustard-cli 42 | yarn add typescript ts-node -D 43 | ``` 44 | ```bash [pnpm] 45 | pnpm i mustard-cli 46 | pnpm i typescript ts-node -D 47 | ``` 48 | :: 49 | 50 | > You can use any executor besides `ts-node`, just like `esno`. Tutorial from this documentation is based on `ts-node` + `nodemon`. 51 | 52 | Create minimial `tsconfig.json` as following: 53 | 54 | ```json 55 | { 56 | "ts-node": { 57 | "swc": false, 58 | "pretty": true, 59 | "transpileOnly": true, 60 | }, 61 | "compilerOptions": { 62 | "target": "ES6", 63 | "module": "ES2022", 64 | "moduleResolution": "node", 65 | "strict": true, 66 | "declaration": false, 67 | "baseUrl": ".", 68 | }, 69 | "include": ["src"], 70 | "exclude": [] 71 | } 72 | ``` 73 | 74 | Add essential parts to `package.json`: 75 | 76 | ```json 77 | { 78 | "name": "awesome-mustard-app", 79 | "version": "0.0.1", 80 | "type": "module", 81 | "main": "index.js", 82 | "bin": { 83 | "awesome-mustard-app": "index.js" 84 | }, 85 | "files": [ 86 | "dist", 87 | "index.js" 88 | ], 89 | "scripts": { 90 | "dev": "nodemon src/index.mts", 91 | "start": "npm run build && nodemon index.js", 92 | "build": "tsc", 93 | }, 94 | "nodemonConfig": { 95 | "delay": 500, 96 | "execMap": { 97 | "js": "node --experimental-specifier-resolution=node", 98 | "ts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm", 99 | "mts": "NODE_OPTIONS='--experimental-specifier-resolution=node' ts-node-esm" 100 | }, 101 | "ext": "ts,mts,js,mjs,json", 102 | "ignore": [ 103 | "**/test/**", 104 | "**/docs/**", 105 | "node_modules" 106 | ], 107 | "verbose": true, 108 | "watch": [ 109 | "*.ts", 110 | "*.mts", 111 | "*.js", 112 | "*.mjs" 113 | ] 114 | }, 115 | "dependencies": { 116 | "mustard-cli": "latest" 117 | }, 118 | "devDependencies": { 119 | "@types/node": "^18.11.7", 120 | "nodemon": "^2.0.20", 121 | "ts-node": "^10.9.1", 122 | "typescript": "^5.0.0" 123 | } 124 | } 125 | ``` 126 | 127 | ::list{type="info"} 128 | **By default Mustard only provides ESM-based modern development workflow, so we need `--experimental-specifier-resolution=node`.** 129 | :: 130 | 131 | Create your first root command in `index.mts`: 132 | 133 | ```typescript 134 | import { MustardFactory } from "mustard-cli"; 135 | import { RootCommand, App, Input } from "mustard-cli/decorator"; 136 | import type { CommandStruct, MustardApp } from "mustard-cli/cli"; 137 | 138 | @RootCommand() 139 | class RootCommandHandle implements CommandStruct { 140 | 141 | @Input() 142 | public name: string = "Harold"; 143 | 144 | public run(): void { 145 | console.log(`Hello, ${this.name}!`); 146 | } 147 | } 148 | 149 | @App({ 150 | name: "mm", 151 | commands: [RootCommandHandle], 152 | }) 153 | class Project implements MustardApp { 154 | onError(error: Error): void { 155 | console.log(error); 156 | } 157 | } 158 | 159 | MustardFactory.init(Project).start(); 160 | ``` 161 | 162 | ```bash 163 | $ nodemon index.mts 164 | Hello, Harold! 165 | 166 | $ nodemon index.mts Ekko 167 | Hello, Ekko! 168 | ``` -------------------------------------------------------------------------------- /packages/docs/content/1.Usage/2.Options.md: -------------------------------------------------------------------------------- 1 | # Options and Inputs 2 | 3 | ## Configuring Options 4 | 5 | When registering a option, you can configure its option name, alias, description, and validators, which Mustard supports in the form of parameter lists or objects: 6 | 7 | ```typescript 8 | import { Validator } from "mustard-cli/validator"; 9 | 10 | @Command('run') 11 | class RunCommandHandle implements CommandStruct { 12 | @Option('message') 13 | @Option('message', 'm') 14 | @Option('message', 'message option') 15 | @Option('message', 'm', 'message option') 16 | @Option('message', Validator.Required().String().MinLength(5)) 17 | @Option({ 18 | name: 'message', 19 | alias: 'm', 20 | description: 'message option', 21 | validator: Validator.Required().String().MinLength(5) 22 | }) 23 | private messageOption: string = 'message default value'; 24 | 25 | public run() {} 26 | } 27 | ``` 28 | 29 | ::list{type="info"} 30 | To split `@Option('message', 'm')` and `@Option('message', 'message option')` overloads, if you provide a string which length is less than 3, it will be treated as alias, otherwise it will be treated as description. 31 | :: 32 | 33 | Property `messageOption` will be parsed as `--message` or `-m` option, and its default value is `message default value`. 34 | 35 | ## Variadic Options 36 | 37 | Mustard supports variadic option by `@VariadicOption` decorator, it will parse all the rest of the command line arguments into an array. And, it can be configured just like `@Option` decorator except `validator`: 38 | 39 | ```typescript 40 | @Command('run') 41 | class RunCommandHandle implements CommandStruct { 42 | @VariadicOption('projects') 43 | @VariadicOption('projects', 'p') 44 | @VariadicOption('projects', 'projects to handle') 45 | @VariadicOption('projects', 'p', 'projects to handle') 46 | @VariadicOption({ 47 | name: 'projects', 48 | alias: 'p', 49 | description: 'projects to handle', 50 | }) 51 | private pprojectsToHandle: string[] = []; 52 | 53 | public run() {} 54 | } 55 | ``` 56 | 57 | Property `pprojectsToHandle` will be parsed as `--projects` or `-p` option, and its default value is `[]`. 58 | 59 | ```bash 60 | $ node index.js run --projects=project1 project2 project3 61 | $ node index.js run --projects=project1 --project=project2 --project=project3 62 | ``` 63 | 64 | ::list{type="info"} 65 | Mustard will use different cli arguments parser libraries depending on the situation, when there's no variadic option and no option alias provided, it will use `yargs-parser` as cli arguments parser , otherwise `mri` will be used for performance improvement. 66 | :: 67 | 68 | ## Validator 69 | 70 | Mustard provide simple validator by `Zod`, you can use it from `Validator` namespace. 71 | 72 | ```typescript 73 | import { Validator } from "mustard-cli/validator"; 74 | 75 | Validator.Required().String().MinLength(5); 76 | Validator.Required().String().StartsWith('node-'); 77 | Validator.Number().Int(); 78 | Validator.Boolean(); 79 | 80 | 81 | enum ValidSource {} 82 | 83 | Validator.Optional().Enum(ValidSource) 84 | ``` 85 | 86 | ## Restrict Values 87 | 88 | ```typescript 89 | type Templates = 'foo' | 'bar'; 90 | 91 | @Command('run') 92 | class RunCommand implements CommandStruct { 93 | 94 | @Option() 95 | template: Template = 'foo'; 96 | 97 | public run() {} 98 | } 99 | ``` 100 | 101 | In command registration above, when we execute command like `bin run --template=baz`, Mustard doesnot validate if it's from pre-defined valid values, and we cannot add validation from `@Validator.Enum()`. 102 | 103 | But this can be a common suitation that we'd like to ensure valid user input for some options: 104 | 105 | - If we received 'foo' or 'bar', use it; 106 | - If we received `undefined`, use the default value; 107 | - If we received any unexpected value, use the default value; 108 | 109 | Decorator `@Restrict` should help in such cases: 110 | 111 | ```typescript 112 | const templates = ['foo', 'bar'] as const; 113 | 114 | type RestrictTemplates = typeof templates[number]; 115 | 116 | @Command('run') 117 | class RunCommand implements CommandStruct { 118 | 119 | @Option() 120 | @Restrict(templates) 121 | template: RestrictTemplates = 'foo'; 122 | 123 | public run() {} 124 | } 125 | ``` 126 | 127 | Usage: 128 | 129 | ```typescript 130 | @Restrict(/* Expected values for this property */) 131 | ``` 132 | 133 | - When `restrictValues` was specified as array, we need to ensure the provided value was included by it. 134 | - When `restrictValues` was an object(Enum usually), we need to ensure the provided value was included in all of its values; -------------------------------------------------------------------------------- /packages/mustard-cli/source/Validators/PrimitiveValidators.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import type { ZodType, ZodBoolean, ZodNumber, ZodString } from "zod"; 4 | 5 | import type { BaseValidator, MaybeOptionalZodType } from "./Typings"; 6 | import type { ValidationTypes } from "../Typings/Shared.struct"; 7 | 8 | export class StringValidator implements BaseValidator, string> { 9 | _schema: ZodString; 10 | 11 | constructor(public required: boolean = false) { 12 | this._schema = z.string(); 13 | } 14 | 15 | public get schema(): MaybeOptionalZodType { 16 | return this.required ? this._schema : this._schema.optional(); 17 | } 18 | 19 | public addValidation(type: ValidationTypes, args: unknown[] = []) { 20 | const validation = { 21 | type, 22 | args, 23 | }; 24 | 25 | // @ts-expect-error 26 | this._schema = this._schema[validation.type]?.(...validation.args); 27 | } 28 | 29 | public validate(value: unknown) { 30 | return this._schema.parse(value); 31 | } 32 | 33 | public MinLength(len: number): Omit { 34 | this.addValidation("min", [len]); 35 | 36 | return this; 37 | } 38 | 39 | public MaxLength(len: number): Omit { 40 | this.addValidation("max", [len]); 41 | return this; 42 | } 43 | 44 | public Length(len: number): Omit { 45 | this.addValidation("length", [len]); 46 | return this; 47 | } 48 | 49 | public Email(): Omit { 50 | this.addValidation("email", []); 51 | return this; 52 | } 53 | 54 | public StartsWith(arg: string): Omit { 55 | this.addValidation("startsWith", [arg]); 56 | return this; 57 | } 58 | 59 | public EndsWith(arg: string): Omit { 60 | this.addValidation("endsWith", [arg]); 61 | return this; 62 | } 63 | } 64 | 65 | export class BooleanValidator 66 | implements BaseValidator, boolean> 67 | { 68 | _schema: ZodBoolean; 69 | 70 | constructor(public required: boolean = false) { 71 | this._schema = z.boolean(); 72 | } 73 | 74 | public get schema() { 75 | return this.required ? this._schema : this._schema.optional(); 76 | } 77 | 78 | public addValidation( 79 | type: ValidationTypes, 80 | args: unknown[] = [] 81 | ) { 82 | const validation = { 83 | type, 84 | args, 85 | }; 86 | 87 | // @ts-expect-error 88 | this._schema = this._schema[validation.type]?.(...validation.args); 89 | } 90 | 91 | public validate(value: unknown) { 92 | return this._schema.parse(value); 93 | } 94 | } 95 | 96 | export class NumberValidator implements BaseValidator, number> { 97 | _schema: ZodNumber; 98 | 99 | constructor(public required: boolean = false) { 100 | this._schema = z.number(); 101 | } 102 | 103 | public get schema(): MaybeOptionalZodType { 104 | return this.required ? this._schema : this._schema.optional(); 105 | } 106 | 107 | public addValidation(type: ValidationTypes, args: unknown[] = []) { 108 | const validation = { 109 | type, 110 | args, 111 | }; 112 | 113 | // @ts-expect-error 114 | this._schema = this._schema[validation.type]?.(...validation.args); 115 | } 116 | 117 | public validate(value: unknown) { 118 | return this._schema.parse(value); 119 | } 120 | 121 | public Gt(compare: number): Omit { 122 | this.addValidation("gt", [compare]); 123 | return this; 124 | } 125 | 126 | public Gte(compare: number): Omit { 127 | this.addValidation("gte", [compare]); 128 | return this; 129 | } 130 | 131 | public Lt(compare: number): Omit { 132 | this.addValidation("lt", [compare]); 133 | return this; 134 | } 135 | 136 | public Lte(compare: number): Omit { 137 | this.addValidation("lte", [compare]); 138 | return this; 139 | } 140 | 141 | public Int(): Omit { 142 | this.addValidation("int", []); 143 | return this; 144 | } 145 | 146 | public Positive(): Omit { 147 | this.addValidation("positive", []); 148 | return this; 149 | } 150 | 151 | public NonPositive(): Omit { 152 | this.addValidation("nonpositive", []); 153 | return this; 154 | } 155 | 156 | public Negative(): Omit { 157 | this.addValidation("negative", []); 158 | return this; 159 | } 160 | 161 | public NonNegative(): Omit { 162 | this.addValidation("nonnegative", []); 163 | return this; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | ![npm dev dependency version](https://img.shields.io/npm/dependency-version/mustard-cli/peer/typescript) 6 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LinbuduLab/Mustard/workflow.yml) 7 | 8 | ![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/LinbuduLab/Mustard?filename=packages%2Fmustard-cli%2Fpackage.json) 9 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/mustard-cli) 10 | [![codecov](https://codecov.io/gh/LinbuduLab/Mustard/branch/main/graph/badge.svg?token=ceNVnMTgmM)](https://codecov.io/gh/LinbuduLab/Mustard) 11 | 12 | IoC & [Native ECMAScript Decorator](https://github.com/tc39/proposal-decorators) based command line app builder. 13 | 14 | ## Requires 15 | 16 | - **Node.js >= 16.0.0** 17 | - **TypeScript >= 5.0.0** 18 | 19 | ## Features 20 | 21 | - Born to be type safe 22 | - Nest command support 23 | - Validator support by [Zod](https://github.com/colinhacks/zod) 24 | - Automatic usage info generation 25 | - Build decoupled applications using IoC concepts 26 | - Essential built-in utils for CLI app 27 | 28 | ## Getting Started 29 | 30 | - [Documentation](https://mustard-cli.netlify.app/) 31 | 32 | ```bash 33 | $ pnpx create-mustard-app 34 | ``` 35 | 36 | Sample with root command only: 37 | 38 | ```typescript 39 | import { MustardFactory, MustardUtils } from "mustard-cli"; 40 | import { RootCommand, App, Input } from "mustard-cli/decorator"; 41 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 42 | 43 | @RootCommand() 44 | class RootCommandHandle implements CommandStruct { 45 | @Input() 46 | public name: string = "Harold"; 47 | 48 | public run(): void { 49 | console.log(`Hi, ${this.name}`); 50 | } 51 | } 52 | 53 | @App({ 54 | name: "hi", 55 | commands: [RootCommandHandle], 56 | }) 57 | class Project implements MustardApp {} 58 | 59 | MustardFactory.init(Project).start(); 60 | ``` 61 | 62 | ```bash 63 | $ hi 64 | # Hi, Harold 65 | $ hi John 66 | # Hi, John 67 | ``` 68 | 69 | Sample with root command and sub commands: 70 | 71 | ```typescript 72 | import { MustardFactory } from "mustard-cli"; 73 | import { 74 | Command, 75 | RootCommand, 76 | Option, 77 | VariadicOption, 78 | App, 79 | Input, 80 | } from "mustard-cli/decorator"; 81 | import { Validator } from "mustard-cli/validator"; 82 | import { CommandStruct, MustardApp } from "mustard-cli/cli"; 83 | 84 | import path from "path"; 85 | 86 | @RootCommand() 87 | class RootCommandHandle implements CommandStruct { 88 | @Option("m") 89 | public msg = "default value of msg"; 90 | 91 | public run(): void { 92 | console.log(`Root command executed with: msg: ${this.msg}`); 93 | } 94 | } 95 | 96 | @Command("update", "u", "update project dependencies") 97 | class UpdateCommand implements CommandStruct { 98 | @Option("depth", "depth of packages to update", Validator.Number().Gte(1)) 99 | public depth = 10; 100 | 101 | @Option(Validator.Boolean()) 102 | public dry = false; 103 | 104 | @Option({ name: "target", alias: "t" }) 105 | public targetOption: string; 106 | 107 | @Input() 108 | public input: string[] = []; 109 | 110 | @VariadicOption() 111 | public packages: string[] = []; 112 | 113 | public run(): void { 114 | console.log( 115 | `Update command executed with: depth: ${this.depth}, dry: ${ 116 | this.dry 117 | }, targetOption: ${this.targetOption}, input: ${JSON.stringify( 118 | this.input 119 | )}, packages: ${JSON.stringify(this.packages)}` 120 | ); 121 | } 122 | } 123 | 124 | @App({ 125 | name: "mm", 126 | commands: [RootCommandHandle, UpdateCommand], 127 | configurations: { 128 | allowUnknownOptions: true, 129 | enableVersion: require(path.resolve("./package.json")).version, 130 | }, 131 | }) 132 | class Project implements MustardApp { 133 | onStart() {} 134 | 135 | onComplete() {} 136 | } 137 | 138 | MustardFactory.init(Project).start(); 139 | ``` 140 | 141 | ```bash 142 | $ mm 143 | # Root command executed with: msg: default value of msg 144 | $ mm -m=hello 145 | # Root command executed with: msg: hello 146 | $ mm update 147 | # Update command executed with: depth: 10, dry: false, targetOption: undefined, input: [], packages: [] 148 | $ mm update --depth=1 --target=dep --packages p1 p2 p3 149 | # Update command executed with: depth: 1, dry: false, targetOption: dep, input: [], packages: ["p1","p2","p3"] 150 | $ mm update p1 p2 p3 -t=dev 151 | # Update command executed with: depth: 10, dry: false, targetOption: dev, input: ["p1","p2","p3"], packages: [] 152 | ``` 153 | 154 | ## Samples 155 | 156 | You can find more samples [Here](packages/sample/samples/). 157 | 158 | ## License 159 | 160 | [MIT](LICENSE) 161 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/NonCompleteParse/Usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports/index"; 2 | import { 3 | Command, 4 | RootCommand, 5 | Option, 6 | VariadicOption, 7 | App, 8 | Input, 9 | Options, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 12 | 13 | @RootCommand() 14 | class RootCommandHandle implements CommandStruct { 15 | @Option() 16 | public pure = "default value of pure"; 17 | 18 | @Option("msg") 19 | public msgOption = "default value of msg"; 20 | 21 | @Input() 22 | public inputs: string; 23 | 24 | @Options() 25 | public options: unknown; 26 | 27 | public run(): void { 28 | console.log("Root Command"); 29 | 30 | console.log(`--pure option: ${this.pure}`); 31 | 32 | console.log(`--msg option: ${this.msgOption}`); 33 | 34 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 35 | 36 | console.log(`options: ${JSON.stringify(this.options)}`); 37 | } 38 | } 39 | 40 | @Command("run", "r") 41 | class RunCommandHandle implements CommandStruct { 42 | @Option() 43 | public pure = "default value of pure"; 44 | 45 | @Option("msg") 46 | public msgOption = "default value of msg"; 47 | 48 | @Input() 49 | public inputs: string; 50 | 51 | @Options() 52 | public options: unknown; 53 | 54 | public run(): void { 55 | console.log("Run Command"); 56 | 57 | console.log(`--pure option: ${this.pure}`); 58 | 59 | console.log(`--msg option: ${this.msgOption}`); 60 | 61 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 62 | 63 | console.log(`options: ${JSON.stringify(this.options)}`); 64 | } 65 | } 66 | 67 | @Command("node", "n") 68 | class UpdateDepNodeCommandHandle implements CommandStruct { 69 | @Option() 70 | public pure = "default value of pure"; 71 | 72 | @Option("msg") 73 | public msgOption = "default value of msg"; 74 | 75 | @Input() 76 | public inputs: string; 77 | 78 | @Options() 79 | public options: unknown; 80 | 81 | public run(): void { 82 | console.log("Update Dep Node Command"); 83 | 84 | console.log(`--pure option: ${this.pure}`); 85 | 86 | console.log(`--msg option: ${this.msgOption}`); 87 | 88 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 89 | 90 | console.log(`options: ${JSON.stringify(this.options)}`); 91 | } 92 | } 93 | 94 | @Command("dep", "d", [UpdateDepNodeCommandHandle]) 95 | class UpdateDepCommandHandle implements CommandStruct { 96 | @Option() 97 | public pure = "default value of pure"; 98 | 99 | @Option("msg") 100 | public msgOption = "default value of msg"; 101 | 102 | @Input() 103 | public inputs: string; 104 | 105 | @Options() 106 | public options: unknown; 107 | 108 | public run(): void { 109 | console.log("Update Dep Command"); 110 | 111 | console.log(`--pure option: ${this.pure}`); 112 | 113 | console.log(`--msg option: ${this.msgOption}`); 114 | 115 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 116 | 117 | console.log(`options: ${JSON.stringify(this.options)}`); 118 | } 119 | } 120 | 121 | @Command("sys", "s", []) 122 | class UpdateSysCommandHandle implements CommandStruct { 123 | @Option() 124 | public pure = "default value of pure"; 125 | 126 | @Option("msg") 127 | public msgOption = "default value of msg"; 128 | 129 | @Input() 130 | public inputs: string; 131 | 132 | @Options() 133 | public options: unknown; 134 | 135 | public run(): void { 136 | console.log("Update Sys Command"); 137 | 138 | console.log(`--pure option: ${this.pure}`); 139 | 140 | console.log(`--msg option: ${this.msgOption}`); 141 | 142 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 143 | 144 | console.log(`options: ${JSON.stringify(this.options)}`); 145 | } 146 | } 147 | 148 | @Command("update", "u", [UpdateDepCommandHandle, UpdateSysCommandHandle]) 149 | class UpdateCommandHandle implements CommandStruct { 150 | @Option("msg") 151 | public msg = "default value of msg"; 152 | 153 | @Input() 154 | public inputs: string; 155 | 156 | @Options() 157 | public options: unknown; 158 | 159 | public run(): void { 160 | console.log("Update Command"); 161 | 162 | console.log(`--msg option: ${this.msg}`); 163 | 164 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 165 | 166 | console.log(`options: ${JSON.stringify(this.options)}`); 167 | } 168 | } 169 | 170 | @App({ 171 | name: "LinbuduLab CLI", 172 | commands: [ 173 | RootCommandHandle, 174 | RunCommandHandle, 175 | UpdateCommandHandle, 176 | UpdateDepCommandHandle, 177 | UpdateSysCommandHandle, 178 | UpdateDepNodeCommandHandle, 179 | ], 180 | configurations: { 181 | allowUnknownOptions: true, 182 | }, 183 | providers: [], 184 | }) 185 | class Project implements MustardApp { 186 | onStart() {} 187 | 188 | onComplete() {} 189 | } 190 | 191 | MustardFactory.init(Project).start(); 192 | -------------------------------------------------------------------------------- /packages/docs/public/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /packages/docs/public/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/Common/Usage.ts: -------------------------------------------------------------------------------- 1 | import { MustardFactory } from "../../../Exports"; 2 | import { 3 | Command, 4 | RootCommand, 5 | Option, 6 | VariadicOption, 7 | App, 8 | Input, 9 | Options, 10 | } from "../../../Exports/Decorators"; 11 | import { CommandStruct, MustardApp } from "../../../Exports/ComanndLine"; 12 | 13 | @RootCommand() 14 | class RootCommandHandle implements CommandStruct { 15 | @Option() 16 | public pure = "default value of pure"; 17 | 18 | @Option("msg", "m") 19 | public msgOption = "default value of msg"; 20 | 21 | @VariadicOption({ alias: "p" }) 22 | public projects: string[] = []; 23 | 24 | @Input() 25 | public inputs: string; 26 | 27 | @Options() 28 | public options: unknown; 29 | 30 | public run(): void { 31 | console.log("Root Command"); 32 | 33 | console.log(`--pure option: ${this.pure}`); 34 | 35 | console.log(`--msg option: ${this.msgOption}`); 36 | 37 | console.log(`--projects option: ${JSON.stringify(this.projects)}`); 38 | 39 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 40 | 41 | console.log(`options: ${JSON.stringify(this.options)}`); 42 | } 43 | } 44 | 45 | @Command("run", "r") 46 | class RunCommandHandle implements CommandStruct { 47 | @Option() 48 | public pure = "default value of pure"; 49 | 50 | @Option("msg", "m") 51 | public msgOption = "default value of msg"; 52 | 53 | @VariadicOption({ alias: "p" }) 54 | public projects: string[] = []; 55 | 56 | @Input() 57 | public inputs: string; 58 | 59 | @Options() 60 | public options: unknown; 61 | 62 | public run(): void { 63 | console.log("Run Command"); 64 | 65 | console.log(`--pure option: ${this.pure}`); 66 | 67 | console.log(`--msg option: ${this.msgOption}`); 68 | 69 | console.log(`--projects option: ${JSON}`); 70 | 71 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 72 | 73 | console.log(`options: ${JSON.stringify(this.options)}`); 74 | } 75 | } 76 | 77 | @Command("node", "n") 78 | class UpdateDepNodeCommandHandle implements CommandStruct { 79 | @Option() 80 | public pure = "default value of pure"; 81 | 82 | @Option("msg", "m") 83 | public msgOption = "default value of msg"; 84 | 85 | @VariadicOption({ alias: "p" }) 86 | public projects: string[] = []; 87 | 88 | @Input() 89 | public inputs: string; 90 | 91 | @Options() 92 | public options: unknown; 93 | 94 | public run(): void { 95 | console.log("Update Dep Node Command"); 96 | 97 | console.log(`--pure option: ${this.pure}`); 98 | 99 | console.log(`--msg option: ${this.msgOption}`); 100 | 101 | console.log(`--projects option: ${JSON}`); 102 | 103 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 104 | 105 | console.log(`options: ${JSON.stringify(this.options)}`); 106 | } 107 | } 108 | 109 | @Command("dep", "d", [UpdateDepNodeCommandHandle]) 110 | class UpdateDepCommandHandle implements CommandStruct { 111 | @Option() 112 | public pure = "default value of pure"; 113 | 114 | @Option("msg", "m") 115 | public msgOption = "default value of msg"; 116 | 117 | @VariadicOption({ alias: "p" }) 118 | public projects: string[] = []; 119 | 120 | @Input() 121 | public inputs: string; 122 | 123 | @Options() 124 | public options: unknown; 125 | 126 | public run(): void { 127 | console.log("Update Dep Command"); 128 | 129 | console.log(`--pure option: ${this.pure}`); 130 | 131 | console.log(`--msg option: ${this.msgOption}`); 132 | 133 | console.log(`--projects option: ${JSON}`); 134 | 135 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 136 | 137 | console.log(`options: ${JSON.stringify(this.options)}`); 138 | } 139 | } 140 | 141 | @Command("sys", "s", []) 142 | class UpdateSysCommandHandle implements CommandStruct { 143 | @Option() 144 | public pure = "default value of pure"; 145 | 146 | @Option("msg", "m") 147 | public msgOption = "default value of msg"; 148 | 149 | @VariadicOption({ alias: "p" }) 150 | public projects: string[] = []; 151 | 152 | @Input() 153 | public inputs: string; 154 | 155 | @Options() 156 | public options: unknown; 157 | 158 | public run(): void { 159 | console.log("Update Sys Command"); 160 | 161 | console.log(`--pure option: ${this.pure}`); 162 | 163 | console.log(`--msg option: ${this.msgOption}`); 164 | 165 | console.log(`--projects option: ${JSON}`); 166 | 167 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 168 | 169 | console.log(`options: ${JSON.stringify(this.options)}`); 170 | } 171 | } 172 | 173 | @Command("update", "u", [UpdateDepCommandHandle, UpdateSysCommandHandle]) 174 | class UpdateCommandHandle implements CommandStruct { 175 | @Option("msg", "m") 176 | public msg = "default value of msg"; 177 | 178 | @VariadicOption({ alias: "p" }) 179 | public projects: string[] = []; 180 | 181 | @Input() 182 | public inputs: string; 183 | 184 | @Options() 185 | public options: unknown; 186 | 187 | public run(): void { 188 | console.log("Update Command"); 189 | 190 | console.log(`--msg option: ${this.msg}`); 191 | 192 | console.log(`--projects option: ${JSON}`); 193 | 194 | console.log(`inputs: ${JSON.stringify(this.inputs)}`); 195 | 196 | console.log(`options: ${JSON.stringify(this.options)}`); 197 | } 198 | } 199 | 200 | @App({ 201 | name: "LinbuduLab CLI", 202 | commands: [ 203 | RootCommandHandle, 204 | RunCommandHandle, 205 | UpdateCommandHandle, 206 | UpdateDepCommandHandle, 207 | UpdateSysCommandHandle, 208 | UpdateDepNodeCommandHandle, 209 | ], 210 | configurations: { 211 | allowUnknownOptions: true, 212 | }, 213 | providers: [], 214 | }) 215 | class Project implements MustardApp { 216 | onStart() {} 217 | 218 | onComplete() {} 219 | } 220 | 221 | MustardFactory.init(Project).start(); 222 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach } from "vitest"; 2 | import { ZodError } from "zod"; 3 | import { Validator } from "../Validators"; 4 | 5 | enum VE { 6 | Foo, 7 | } 8 | 9 | describe("Validators", () => { 10 | it("should mark as optional by default", () => { 11 | expect(Validator.required).toBeFalsy(); 12 | expect(Validator.Required().required).toBeTruthy(); 13 | expect(Validator.Optional().required).toBeFalsy(); 14 | }); 15 | it("should produce REQUIRED Validator", () => { 16 | try { 17 | expect(Validator.Required().String().validate(undefined)); 18 | } catch (error) { 19 | expect(error).toBeInstanceOf(ZodError); 20 | } 21 | 22 | try { 23 | expect(Validator.Required().Boolean().validate(undefined)); 24 | } catch (error) { 25 | expect(error).toBeInstanceOf(ZodError); 26 | } 27 | 28 | try { 29 | expect(Validator.Required().Number().validate(undefined)); 30 | } catch (error) { 31 | expect(error).toBeInstanceOf(ZodError); 32 | } 33 | 34 | try { 35 | expect(Validator.Required().Enum(VE).validate(undefined)); 36 | } catch (error) { 37 | expect(error).toBeInstanceOf(ZodError); 38 | } 39 | }); 40 | 41 | it("should produce OPTIONAL Validator", () => { 42 | expect(Validator.String().validate("str")).toBe("str"); 43 | expect(Validator.Boolean().validate(true)).toBeTruthy(); 44 | expect(Validator.Number().validate(599)).toBe(599); 45 | expect(Validator.Enum(VE).validate(0)).toBe(0); 46 | expect(Validator.Enum(VE).validate(VE.Foo)).toBe(VE.Foo); 47 | 48 | try { 49 | expect(Validator.Required().String().validate(null)); 50 | } catch (error) { 51 | expect(error).toBeInstanceOf(ZodError); 52 | } 53 | 54 | try { 55 | expect(Validator.Required().Boolean().validate(null)); 56 | } catch (error) { 57 | expect(error).toBeInstanceOf(ZodError); 58 | } 59 | 60 | try { 61 | expect(Validator.Required().Number().validate(null)); 62 | } catch (error) { 63 | expect(error).toBeInstanceOf(ZodError); 64 | } 65 | 66 | try { 67 | expect(Validator.Required().Enum(VE).validate(null)); 68 | } catch (error) { 69 | expect(error).toBeInstanceOf(ZodError); 70 | } 71 | }); 72 | }); 73 | 74 | describe("Validators.String", () => { 75 | it("should produce schema", () => { 76 | expect(Validator.String().schema.isOptional()).toBeTruthy(); 77 | expect(Validator.Required().String().schema.isOptional()).toBeFalsy(); 78 | }); 79 | 80 | it("should validate string primitive", () => { 81 | expect(Validator.String().validate("str")).toBe("str"); 82 | try { 83 | expect(Validator.String().Length(6).validate("str")); 84 | } catch (error) { 85 | expect(error).toBeInstanceOf(ZodError); 86 | } 87 | 88 | try { 89 | expect(Validator.String().StartsWith("foo").validate("str")); 90 | } catch (error) { 91 | expect(error).toBeInstanceOf(ZodError); 92 | } 93 | 94 | try { 95 | expect(Validator.String().EndsWith("foo").validate("str")); 96 | } catch (error) { 97 | expect(error).toBeInstanceOf(ZodError); 98 | } 99 | 100 | try { 101 | expect(Validator.String().MinLength(6).validate("str")); 102 | } catch (error) { 103 | expect(error).toBeInstanceOf(ZodError); 104 | } 105 | 106 | try { 107 | expect(Validator.String().MaxLength(1).validate("str")); 108 | } catch (error) { 109 | expect(error).toBeInstanceOf(ZodError); 110 | } 111 | 112 | try { 113 | expect(Validator.String().Email().validate("str")); 114 | } catch (error) { 115 | expect(error).toBeInstanceOf(ZodError); 116 | } 117 | }); 118 | }); 119 | 120 | describe("Validators.Number", () => { 121 | it("should produce schema", () => { 122 | expect(Validator.Number().schema.isOptional()).toBeTruthy(); 123 | expect(Validator.Required().Number().schema.isOptional()).toBeFalsy(); 124 | }); 125 | 126 | it("should validate number primitive", () => { 127 | try { 128 | expect(Validator.Number().Positive().validate(-1)); 129 | } catch (error) { 130 | expect(error).toBeInstanceOf(ZodError); 131 | } 132 | 133 | try { 134 | expect(Validator.Number().NonPositive().validate(1)); 135 | } catch (error) { 136 | expect(error).toBeInstanceOf(ZodError); 137 | } 138 | 139 | try { 140 | expect(Validator.Number().Negative().validate(-1)); 141 | } catch (error) { 142 | expect(error).toBeInstanceOf(ZodError); 143 | } 144 | 145 | try { 146 | expect(Validator.Number().NonNegative().validate(1)); 147 | } catch (error) { 148 | expect(error).toBeInstanceOf(ZodError); 149 | } 150 | 151 | try { 152 | expect(Validator.Number().Int().validate(1.89)); 153 | } catch (error) { 154 | expect(error).toBeInstanceOf(ZodError); 155 | } 156 | 157 | try { 158 | expect(Validator.Number().Gt(10).validate(-1)); 159 | } catch (error) { 160 | expect(error).toBeInstanceOf(ZodError); 161 | } 162 | 163 | try { 164 | expect(Validator.Number().Gte(10).validate(-1)); 165 | } catch (error) { 166 | expect(error).toBeInstanceOf(ZodError); 167 | } 168 | 169 | try { 170 | expect(Validator.Number().Lt(10).validate(11)); 171 | } catch (error) { 172 | expect(error).toBeInstanceOf(ZodError); 173 | } 174 | 175 | try { 176 | expect(Validator.Number().Lte(10).validate(11)); 177 | } catch (error) { 178 | expect(error).toBeInstanceOf(ZodError); 179 | } 180 | }); 181 | }); 182 | 183 | describe("Validators.Enum", () => { 184 | it("should produce schema", () => { 185 | expect(Validator.Enum(VE).schema.isOptional()).toBeTruthy(); 186 | expect(Validator.Required().Enum(VE).schema.isOptional()).toBeFalsy(); 187 | }); 188 | it("should validate native enum type", () => { 189 | try { 190 | expect(Validator.Enum(VE).validate(11)); 191 | } catch (error) { 192 | expect(error).toBeInstanceOf(ZodError); 193 | } 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/BuiltInCommands.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommandRegistryPayload, 3 | CommandStruct, 4 | } from "../Typings/Command.struct"; 5 | import { describe, it, expect, vi, beforeEach } from "vitest"; 6 | import { BuiltInCommands } from "../Commands/BuiltInCommands"; 7 | import { MustardConstanst } from "../Components/Constants"; 8 | import { UsageInfoGenerator } from "../Components/UsageGenerator"; 9 | 10 | vi.spyOn(UsageInfoGenerator, "printHelp").mockImplementation(() => {}); 11 | vi.spyOn(UsageInfoGenerator, "initGenerator").mockImplementation(() => {}); 12 | 13 | class Foo implements CommandStruct { 14 | run() {} 15 | } 16 | 17 | const registration = { 18 | commandInvokeName: "foo", 19 | root: false, 20 | Class: Foo, 21 | instance: new Foo(), 22 | childCommandList: [], 23 | decoratedInstanceFields: [], 24 | } satisfies CommandRegistryPayload; 25 | 26 | describe("BuiltInCommands", () => { 27 | it("should check if contains help flag", () => { 28 | expect(BuiltInCommands.containsHelpFlag({ _: [], help: true })).toBe(true); 29 | expect(BuiltInCommands.containsHelpFlag({ _: [], h: true })).toBe(true); 30 | expect( 31 | BuiltInCommands.containsHelpFlag({ 32 | _: [], 33 | [MustardConstanst.InternalHelpFlag]: true, 34 | }) 35 | ).toBe(true); 36 | expect( 37 | BuiltInCommands.containsHelpFlag({ 38 | _: [], 39 | }) 40 | ).toBe(false); 41 | expect( 42 | BuiltInCommands.containsHelpFlag({ 43 | _: [], 44 | foo: true, 45 | }) 46 | ).toBe(false); 47 | }); 48 | 49 | it("should check if contains version flag", () => { 50 | expect(BuiltInCommands.containsVersionFlag({ _: [], version: true })).toBe( 51 | true 52 | ); 53 | expect(BuiltInCommands.containsVersionFlag({ _: [], v: true })).toBe(true); 54 | expect( 55 | BuiltInCommands.containsVersionFlag({ 56 | _: [], 57 | [MustardConstanst.InternalVersionFlag]: true, 58 | }) 59 | ).toBe(true); 60 | expect( 61 | BuiltInCommands.containsVersionFlag({ 62 | _: [], 63 | }) 64 | ).toBe(false); 65 | expect( 66 | BuiltInCommands.containsVersionFlag({ 67 | _: [], 68 | foo: true, 69 | }) 70 | ).toBe(false); 71 | }); 72 | 73 | it("should handle help command", () => { 74 | BuiltInCommands.useHelpCommand("mm", false, undefined, undefined, false); 75 | expect(UsageInfoGenerator.printHelp).not.toBeCalled(); 76 | 77 | expect(UsageInfoGenerator.initGenerator).not.toBeCalled(); 78 | 79 | BuiltInCommands.useHelpCommand( 80 | "mm", 81 | { 82 | _: [], 83 | help: false, 84 | }, 85 | undefined, 86 | false, 87 | false 88 | ); 89 | expect(UsageInfoGenerator.printHelp).not.toBeCalled(); 90 | expect(UsageInfoGenerator.initGenerator).not.toBeCalled(); 91 | 92 | BuiltInCommands.useHelpCommand("mm", true, undefined, false, false); 93 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(1); 94 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 95 | bin: "mm", 96 | parsedInputs: [], 97 | }); 98 | 99 | BuiltInCommands.useHelpCommand( 100 | "mm", 101 | { _: [], help: true }, 102 | undefined, 103 | false, 104 | false 105 | ); 106 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(2); 107 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 108 | bin: "mm", 109 | parsedInputs: [], 110 | }); 111 | 112 | BuiltInCommands.useHelpCommand("mm", true, registration, false, false); 113 | 114 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(3); 115 | expect(UsageInfoGenerator.printHelp).toHaveBeenLastCalledWith(registration); 116 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 117 | bin: "mm", 118 | parsedInputs: [], 119 | }); 120 | 121 | BuiltInCommands.useHelpCommand( 122 | "mm", 123 | { _: [], help: true }, 124 | registration, 125 | false, 126 | false 127 | ); 128 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(4); 129 | expect(UsageInfoGenerator.printHelp).toHaveBeenLastCalledWith(registration); 130 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 131 | bin: "mm", 132 | parsedInputs: [], 133 | }); 134 | 135 | BuiltInCommands.useHelpCommand( 136 | "mm", 137 | { _: [], help: true }, 138 | registration, 139 | true, 140 | false 141 | ); 142 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(5); 143 | expect(UsageInfoGenerator.printHelp).toHaveBeenLastCalledWith(registration); 144 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 145 | bin: "mm", 146 | parsedInputs: [], 147 | }); 148 | 149 | vi.spyOn(console, "log").mockImplementationOnce(() => {}); 150 | 151 | BuiltInCommands.useHelpCommand( 152 | "mm", 153 | { _: ["foo"], help: true }, 154 | registration, 155 | () => "FromController", 156 | false 157 | ); 158 | expect(UsageInfoGenerator.printHelp).toBeCalledTimes(5); 159 | expect(UsageInfoGenerator.initGenerator).toHaveBeenLastCalledWith({ 160 | bin: "mm", 161 | parsedInputs: ["foo"], 162 | }); 163 | 164 | expect(console.log).toBeCalledWith("FromController"); 165 | }); 166 | 167 | it("should handle version command", () => { 168 | const controller = vi.fn().mockReturnValue("1.0.0"); 169 | vi.spyOn(console, "log").mockImplementation(() => {}); 170 | 171 | BuiltInCommands.useVersionCommand(false, controller, false); 172 | expect(controller).not.toBeCalled(); 173 | 174 | BuiltInCommands.useVersionCommand(false, false, false); 175 | expect(console.log).toBeCalledTimes(0); 176 | 177 | BuiltInCommands.useVersionCommand(true, controller, false); 178 | expect(controller).toBeCalledTimes(1); 179 | expect(console.log).toBeCalledTimes(1); 180 | 181 | BuiltInCommands.useVersionCommand( 182 | { _: [], version: true }, 183 | controller, 184 | false 185 | ); 186 | expect(console.log).toBeCalledTimes(2); 187 | expect(controller).toBeCalledTimes(2); 188 | 189 | BuiltInCommands.useVersionCommand(true, "1.2.0", false); 190 | expect(controller).toBeCalledTimes(2); 191 | expect(console.log).toBeCalledTimes(3); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/__tests__/Integrations/NonCompleteParse/NonCompleteParse.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | import { execaCommand } from "execa"; 3 | import { TestHelper } from "../../Fixtures/TestHelper"; 4 | import path from "path"; 5 | 6 | const UsagePath = path.resolve(__dirname, "./Usage.ts"); 7 | 8 | describe("IntegrationTesting:NonCompleteParse", () => { 9 | it("should dispatch command", async () => { 10 | const { stdout: stdoutWithRoot1 } = await execaCommand( 11 | `${TestHelper.IntegrationExecutor} ${UsagePath}` 12 | ); 13 | expect(stdoutWithRoot1).toMatchInlineSnapshot( 14 | ` 15 | "Root Command 16 | --pure option: default value of pure 17 | --msg option: default value of msg 18 | inputs: [] 19 | options: {\\"pure\\":\\"default value of pure\\",\\"msg\\":\\"default value of msg\\"}" 20 | ` 21 | ); 22 | 23 | const { stdout: stdoutWithRoot2 } = await execaCommand( 24 | `${TestHelper.IntegrationExecutor} ${UsagePath} input1 input2 input3` 25 | ); 26 | expect(stdoutWithRoot2).toMatchInlineSnapshot( 27 | ` 28 | "Root Command 29 | --pure option: default value of pure 30 | --msg option: default value of msg 31 | inputs: [\\"input1\\",\\"input2\\",\\"input3\\"] 32 | options: {\\"pure\\":\\"default value of pure\\",\\"msg\\":\\"default value of msg\\"}" 33 | ` 34 | ); 35 | 36 | const { stdout: stdoutWithRoot3 } = await execaCommand( 37 | `${TestHelper.IntegrationExecutor} ${UsagePath} input1 input2 input3 --msg Hello --pure pureValue` 38 | ); 39 | expect(stdoutWithRoot3).toMatchInlineSnapshot( 40 | ` 41 | "Root Command 42 | --pure option: pureValue 43 | --msg option: Hello 44 | inputs: [\\"input1\\",\\"input2\\",\\"input3\\"] 45 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 46 | ` 47 | ); 48 | 49 | const { stdout: stdout1 } = await execaCommand( 50 | `${TestHelper.IntegrationExecutor} ${UsagePath} run` 51 | ); 52 | expect(stdout1).toMatchInlineSnapshot( 53 | ` 54 | "Run Command 55 | --pure option: default value of pure 56 | --msg option: default value of msg 57 | inputs: [] 58 | options: {\\"pure\\":\\"default value of pure\\",\\"msg\\":\\"default value of msg\\"}" 59 | ` 60 | ); 61 | 62 | const { stdout: stdout2 } = await execaCommand( 63 | `${TestHelper.IntegrationExecutor} ${UsagePath} run input1 input2 --msg Hello --pure pureValue` 64 | ); 65 | expect(stdout2).toMatchInlineSnapshot( 66 | ` 67 | "Run Command 68 | --pure option: pureValue 69 | --msg option: Hello 70 | inputs: [\\"input1\\",\\"input2\\"] 71 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 72 | ` 73 | ); 74 | 75 | const { stdout: stdout3 } = await execaCommand( 76 | `${TestHelper.IntegrationExecutor} ${UsagePath} update input1 input2 --msg Hello --pure pureValue` 77 | ); 78 | expect(stdout3).toMatchInlineSnapshot( 79 | ` 80 | "Update Command 81 | --msg option: Hello 82 | inputs: [\\"input1\\",\\"input2\\"] 83 | options: {\\"msg\\":\\"Hello\\"}" 84 | ` 85 | ); 86 | 87 | const { stdout: stdout4 } = await execaCommand( 88 | `${TestHelper.IntegrationExecutor} ${UsagePath} update input1 input2 --msg Hello --pure pureValue` 89 | ); 90 | expect(stdout4).toMatchInlineSnapshot( 91 | ` 92 | "Update Command 93 | --msg option: Hello 94 | inputs: [\\"input1\\",\\"input2\\"] 95 | options: {\\"msg\\":\\"Hello\\"}" 96 | ` 97 | ); 98 | 99 | const { stdout: stdout5 } = await execaCommand( 100 | `${TestHelper.IntegrationExecutor} ${UsagePath} update dep input1 input2 --msg Hello --pure pureValue` 101 | ); 102 | expect(stdout5).toMatchInlineSnapshot( 103 | ` 104 | "Update Dep Command 105 | --pure option: pureValue 106 | --msg option: Hello 107 | inputs: [\\"input1\\",\\"input2\\"] 108 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 109 | ` 110 | ); 111 | 112 | const { stdout: stdout6 } = await execaCommand( 113 | `${TestHelper.IntegrationExecutor} ${UsagePath} update dep node input1 input2 --msg Hello --pure pureValue` 114 | ); 115 | expect(stdout6).toMatchInlineSnapshot( 116 | ` 117 | "Update Dep Node Command 118 | --pure option: pureValue 119 | --msg option: Hello 120 | inputs: [\\"input1\\",\\"input2\\"] 121 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 122 | ` 123 | ); 124 | 125 | const { stdout: stdout7 } = await execaCommand( 126 | `${TestHelper.IntegrationExecutor} ${UsagePath} update sys input1 input2 --msg Hello --pure pureValue` 127 | ); 128 | expect(stdout7).toMatchInlineSnapshot( 129 | ` 130 | "Update Sys Command 131 | --pure option: pureValue 132 | --msg option: Hello 133 | inputs: [\\"input1\\",\\"input2\\"] 134 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 135 | ` 136 | ); 137 | 138 | const { stdout: stdout8 } = await execaCommand( 139 | `${TestHelper.IntegrationExecutor} ${UsagePath} run` 140 | ); 141 | expect(stdout8).toMatchInlineSnapshot( 142 | ` 143 | "Run Command 144 | --pure option: default value of pure 145 | --msg option: default value of msg 146 | inputs: [] 147 | options: {\\"pure\\":\\"default value of pure\\",\\"msg\\":\\"default value of msg\\"}" 148 | ` 149 | ); 150 | 151 | const { stdout: stdout9 } = await execaCommand( 152 | `${TestHelper.IntegrationExecutor} ${UsagePath} r input1 input2 --msg Hello --pure pureValue --projects app1 app2 app3 --projects app` 153 | ); 154 | expect(stdout9).toMatchInlineSnapshot( 155 | ` 156 | "Run Command 157 | --pure option: pureValue 158 | --msg option: Hello 159 | inputs: [\\"input1\\",\\"input2\\",\\"app2\\",\\"app3\\"] 160 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 161 | ` 162 | ); 163 | 164 | const { stdout: stdout10 } = await execaCommand( 165 | `${TestHelper.IntegrationExecutor} ${UsagePath} u d input1 input2 --msg Hello --pure pureValue` 166 | ); 167 | expect(stdout10).toMatchInlineSnapshot( 168 | ` 169 | "Update Dep Command 170 | --pure option: pureValue 171 | --msg option: Hello 172 | inputs: [\\"input1\\",\\"input2\\"] 173 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 174 | ` 175 | ); 176 | 177 | const { stdout: stdout11 } = await execaCommand( 178 | `${TestHelper.IntegrationExecutor} ${UsagePath} u d n input1 input2 --msg Hello --pure pureValue` 179 | ); 180 | expect(stdout11).toMatchInlineSnapshot( 181 | ` 182 | "Update Dep Node Command 183 | --pure option: pureValue 184 | --msg option: Hello 185 | inputs: [\\"input1\\",\\"input2\\"] 186 | options: {\\"pure\\":\\"pureValue\\",\\"msg\\":\\"Hello\\"}" 187 | ` 188 | ); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/Utils.ts: -------------------------------------------------------------------------------- 1 | import mri from "mri"; 2 | import uniqby from "lodash.uniqby"; 3 | import parse from "yargs-parser"; 4 | import { closest } from "fastest-levenshtein"; 5 | import { MustardRegistry } from "./Registry"; 6 | import { MustardConstanst } from "./Constants"; 7 | 8 | import type { 9 | CommandInput, 10 | CommandRegistryPayload, 11 | CommandStruct, 12 | } from "../Typings/Command.struct"; 13 | import type { TaggedDecoratedInstanceFields } from "../Typings/Utils.struct"; 14 | import type { Constructable, Dictionary } from "../Typings/Shared.struct"; 15 | import type { OptionInitializerPlaceHolder } from "../Typings/Option.struct"; 16 | import type { RestrictValueSet } from "../Typings/Controller.struct"; 17 | import type { CommandList } from "../Typings/Configuration.struct"; 18 | 19 | export class MustardUtils { 20 | public static getInstanceFields(instance: CommandStruct): string[] { 21 | return Reflect.ownKeys(instance); 22 | } 23 | 24 | public static getInstanceFieldValue( 25 | instance: CommandStruct, 26 | field: string 27 | ): TExpected { 28 | return Reflect.get(instance, field); 29 | } 30 | 31 | public static setInstanceFieldValue( 32 | instance: CommandStruct, 33 | field: string, 34 | value: T 35 | ) { 36 | Reflect.set(instance, field, value); 37 | 38 | return MustardUtils.getInstanceFieldValue(instance, field); 39 | } 40 | 41 | public static parseFromProcessArgs( 42 | withVariadic: string[] = [], 43 | 44 | aliasMap: Dictionary = {} 45 | ) { 46 | const useCompleteParse = Boolean( 47 | withVariadic.length || Object.keys(aliasMap).length 48 | ); 49 | 50 | const parsed = useCompleteParse 51 | ? parse(process.argv.slice(2), { 52 | array: Array.from(withVariadic), 53 | alias: aliasMap, 54 | configuration: { 55 | "greedy-arrays": true, 56 | "strip-aliased": true, 57 | }, 58 | }) 59 | : mri(process.argv.slice(2)); 60 | 61 | return parsed; 62 | } 63 | 64 | public static filterDecoratedInstanceFields( 65 | instance: CommandStruct 66 | ): TaggedDecoratedInstanceFields[] { 67 | const fields = MustardUtils.getInstanceFields(instance); 68 | 69 | return fields 70 | .map((field: string) => { 71 | const value = ( 72 | MustardUtils.getInstanceFieldValue(instance, field) 73 | ); 74 | 75 | if ( 76 | MustardConstanst.InstanceFieldDecorationTypes.includes(value.type) 77 | ) { 78 | return { 79 | key: field, 80 | type: value.type, 81 | value, 82 | }; 83 | } 84 | 85 | return null; 86 | }) 87 | .filter(Boolean); 88 | } 89 | 90 | public static findHandlerCommandWithInputs( 91 | inputs: CommandInput | string[], 92 | commands: string[] = Array.from(MustardRegistry.provide().keys()), 93 | fallback: CommandRegistryPayload = MustardRegistry.provideRootCommand() 94 | ): { 95 | command?: CommandRegistryPayload; 96 | inputs: string[]; 97 | } { 98 | const [matcher, ...rest] = inputs; 99 | 100 | // match command from first input 101 | const matchFromFirstInput = MustardRegistry.provide().get(matcher); 102 | 103 | // if only one input is provided, use it directly 104 | if (inputs.length === 1) { 105 | return { 106 | // lookup common commands first, or return fallback(will be RootCommand at first) 107 | command: matchFromFirstInput ?? fallback, 108 | inputs: matchFromFirstInput ? [] : inputs, 109 | }; 110 | } 111 | 112 | // if more than 1 inputs provided but no matched for first input, return fallback 113 | if (!matchFromFirstInput) { 114 | return { 115 | command: fallback, 116 | inputs: inputs, 117 | }; 118 | } 119 | 120 | // map to get ChildCommand registration 121 | const childCommands = ( 122 | matchFromFirstInput?.childCommandList ?? [] 123 | ) 124 | .map((C) => { 125 | const matched = commands.find((commandIdentifier) => { 126 | const registered = MustardRegistry.provide(commandIdentifier)?.Class; 127 | 128 | return typeof registered !== "undefined" && registered === C; 129 | }); 130 | 131 | return matched; 132 | }) 133 | .filter(Boolean); 134 | 135 | // if no child commands registered, use first matched 136 | if (!childCommands.length) { 137 | return { 138 | command: matchFromFirstInput ?? fallback, 139 | // use rest inputs if matched first input successfully 140 | inputs: matchFromFirstInput ? rest : inputs, 141 | }; 142 | } 143 | 144 | // do this recursively till no more inputs 145 | return MustardUtils.findHandlerCommandWithInputs( 146 | rest, 147 | childCommands.concat([...rest]), 148 | matchFromFirstInput 149 | ); 150 | } 151 | 152 | public static ensureArray(providers: T | T[]): T[] { 153 | return Array.isArray(providers) ? providers : [providers]; 154 | } 155 | 156 | public static isPromise(obj: any): obj is Promise { 157 | return ( 158 | !!obj && 159 | (typeof obj === "object" || typeof obj === "function") && 160 | typeof obj.then === "function" 161 | ); 162 | } 163 | 164 | public static isConstructable(input: any): input is Constructable { 165 | try { 166 | Reflect.construct(String, [], input); 167 | } catch (e) { 168 | return false; 169 | } 170 | return true; 171 | } 172 | 173 | public static levenshtein( 174 | unknownOption: string, 175 | avaliableOptions: string[] = [] 176 | ): string { 177 | return closest(unknownOption, avaliableOptions); 178 | } 179 | 180 | public static isOptionInitializer( 181 | input: any 182 | ): input is OptionInitializerPlaceHolder { 183 | return typeof input === "object" && input.type === "Option"; 184 | } 185 | 186 | public static applyRestrictions( 187 | inputValue: unknown, 188 | defaultValue: unknown, 189 | restrictions?: RestrictValueSet 190 | ) { 191 | if (!restrictions) return inputValue; 192 | 193 | const restrictValues = Array.isArray(restrictions) 194 | ? restrictions 195 | : Object.values(restrictions ?? {}); 196 | 197 | return restrictValues.includes(inputValue) ? inputValue : defaultValue; 198 | } 199 | 200 | public static matchFromCommandClass( 201 | commandClassList: CommandList 202 | ): CommandRegistryPayload[] { 203 | const commandNameList = commandClassList.map((C) => C.name); 204 | 205 | const completeRegistration = MustardRegistry.provide(); 206 | 207 | const matched = Array.from(completeRegistration.values()).filter( 208 | (registration) => { 209 | return ( 210 | typeof registration !== "undefined" && 211 | commandNameList.includes(registration.Class.name) 212 | ); 213 | } 214 | ); 215 | 216 | return uniqby(matched, (registration) => registration.Class.name); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Components/MustardUtilsProvider.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import fsp from "fs/promises"; 3 | import tty from "tty"; 4 | import { EOL } from "os"; 5 | 6 | import type { Nullable } from "../Typings/Shared.struct"; 7 | 8 | interface ReadJsonOptions { 9 | encoding?: null | undefined; 10 | flag?: string | undefined; 11 | throw?: boolean; 12 | parserOptions?: string; 13 | reviver?: (key: string, value: any) => any; 14 | } 15 | 16 | export class JSONHelper { 17 | public static async readJson>( 18 | filePath: string, 19 | options?: Nullable 20 | ): Promise { 21 | return new Promise((resolve, reject) => { 22 | fsp.readFile(filePath).then((content) => { 23 | let parsed: any; 24 | try { 25 | parsed = JSON.parse( 26 | content.toString().replace(/^\uFEFF/, ""), 27 | options?.reviver 28 | ); 29 | resolve(parsed); 30 | } catch (error: any) { 31 | if (options?.throw) { 32 | error.message = `${filePath}: ${error.message}`; 33 | reject(error); 34 | } else { 35 | resolve({} as TParsedContent); 36 | } 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | public static readJsonSync>( 43 | filePath: string, 44 | options?: Nullable 45 | ): TParsedContent { 46 | const content = fs 47 | .readFileSync(filePath, { 48 | ...options, 49 | encoding: "utf-8", 50 | }) 51 | .replace(/^\uFEFF/, ""); 52 | 53 | let parsed: any; 54 | 55 | try { 56 | parsed = JSON.parse(content, options?.reviver); 57 | } catch (error: any) { 58 | if (options?.throw) { 59 | error.message = `${filePath}: ${error.message}`; 60 | throw error; 61 | } else { 62 | return {} as TParsedContent; 63 | } 64 | } 65 | 66 | return parsed; 67 | } 68 | 69 | public static async writeJson( 70 | filePath: string, 71 | content: any, 72 | options?: fs.WriteFileOptions 73 | ): Promise { 74 | return new Promise((resolve, reject) => { 75 | const contentStr = 76 | JSON.stringify(content, null, 2).replace(/\n/g, EOL) + EOL; 77 | 78 | fsp.writeFile(filePath, contentStr, options).then(resolve).catch(reject); 79 | }); 80 | } 81 | 82 | public static writeJsonSync( 83 | filePath: string, 84 | content: any, 85 | options?: fs.WriteFileOptions 86 | ): void { 87 | const contentStr = 88 | JSON.stringify(content, null, 2).replace(/\n/g, EOL) + EOL; 89 | 90 | fs.writeFileSync(filePath, contentStr, options); 91 | } 92 | } 93 | 94 | // source: https://github.com/alexeyraspopov/picocolors/blob/main/picocolors.js 95 | export class ColorsHelper { 96 | public static get isColorSupported(): boolean { 97 | return ( 98 | !("NO_COLOR" in process.env || process.argv.includes("--no-color")) && 99 | ("FORCE_COLOR" in process.env || 100 | process.argv.includes("--color") || 101 | process.platform === "win32" || 102 | (tty.isatty(1) && process.env["TERM"] !== "dumb") || 103 | "CI" in process.env) 104 | ); 105 | } 106 | 107 | private static replaceClose = ( 108 | string: string, 109 | close: string, 110 | replace: string, 111 | index: number 112 | ): string => { 113 | let start = string.substring(0, index) + replace; 114 | let end = string.substring(index + close.length); 115 | let nextIndex = end.indexOf(close); 116 | return ~nextIndex 117 | ? start + ColorsHelper.replaceClose(end, close, replace, nextIndex) 118 | : start + end; 119 | }; 120 | 121 | private static formatter = 122 | (open: string, close: string, replace: string = open) => 123 | (input: string) => { 124 | let string = "" + input; 125 | let index = string.indexOf(close, open.length); 126 | return ~index 127 | ? open + 128 | ColorsHelper.replaceClose(string, close, replace, index) + 129 | close 130 | : open + string + close; 131 | }; 132 | 133 | private static factory = (open: string): ((input: string) => string) => { 134 | return ColorsHelper.isColorSupported 135 | ? ColorsHelper.formatter(open, "\x1b[39m") 136 | : String; 137 | }; 138 | 139 | private static bgFactory = (open: string): ((input: string) => string) => { 140 | return ColorsHelper.isColorSupported 141 | ? ColorsHelper.formatter(open, "\x1b[49m") 142 | : String; 143 | }; 144 | 145 | public static bold = ColorsHelper.isColorSupported 146 | ? ColorsHelper.formatter("\x1b[1m", "\x1b[22m", "\x1b[22m\x1b[1m") 147 | : String; 148 | public static dim = ColorsHelper.isColorSupported 149 | ? ColorsHelper.formatter("\x1b[2m", "\x1b[22m", "\x1b[22m\x1b[2m") 150 | : String; 151 | public static italic = ColorsHelper.isColorSupported 152 | ? ColorsHelper.formatter("\x1b[3m", "\x1b[23m") 153 | : String; 154 | public static underline = ColorsHelper.isColorSupported 155 | ? ColorsHelper.formatter("\x1b[4m", "\x1b[24m") 156 | : String; 157 | public static inverse = ColorsHelper.isColorSupported 158 | ? ColorsHelper.formatter("\x1b[7m", "\x1b[27m") 159 | : String; 160 | public static hidden = ColorsHelper.isColorSupported 161 | ? ColorsHelper.formatter("\x1b[8m", "\x1b[28m") 162 | : String; 163 | public static strikethrough = ColorsHelper.isColorSupported 164 | ? ColorsHelper.formatter("\x1b[9m", "\x1b[29m") 165 | : String; 166 | 167 | public static black = ColorsHelper.factory("\x1b[30m"); 168 | 169 | public static red = ColorsHelper.factory("\x1b[31m"); 170 | 171 | public static green = ColorsHelper.factory("\x1b[32m"); 172 | 173 | public static yellow = ColorsHelper.factory("\x1b[33m"); 174 | 175 | public static blue = ColorsHelper.factory("\x1b[34m"); 176 | 177 | public static magenta = ColorsHelper.factory("\x1b[35m"); 178 | 179 | public static cyan = ColorsHelper.factory("\x1b[36m"); 180 | 181 | public static white = ColorsHelper.factory("\x1b[37m"); 182 | 183 | public static gray = ColorsHelper.factory("\x1b[90m"); 184 | 185 | public static bgRed = ColorsHelper.bgFactory("\x1b[41m"); 186 | 187 | public static bgGreen = ColorsHelper.bgFactory("\x1b[42m"); 188 | 189 | public static bgYellow = ColorsHelper.bgFactory("\x1b[43m"); 190 | 191 | public static bgBlue = ColorsHelper.bgFactory("\x1b[44m"); 192 | 193 | public static bgMagenta = ColorsHelper.bgFactory("\x1b[45m"); 194 | 195 | public static bgCyan = ColorsHelper.bgFactory("\x1b[46m"); 196 | 197 | public static bgWhite = ColorsHelper.bgFactory("\x1b[47m"); 198 | } 199 | 200 | /** 201 | * @Utils 202 | * @Utils.Json 203 | * @Utils.PackageJson 204 | */ 205 | export class MustardUtilsProvider { 206 | public static produce() { 207 | return { 208 | json: MustardUtilsProvider.json, 209 | colors: MustardUtilsProvider.colors, 210 | }; 211 | } 212 | 213 | public static get json() { 214 | return { 215 | readSync: JSONHelper.readJsonSync, 216 | read: JSONHelper.readJson, 217 | writeSync: JSONHelper.writeJsonSync, 218 | write: JSONHelper.writeJson, 219 | }; 220 | } 221 | 222 | public static get colors() { 223 | return ColorsHelper; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /packages/mustard-cli/source/Commands/CommandLine.ts: -------------------------------------------------------------------------------- 1 | import _debug from "debug"; 2 | 3 | import { MustardRegistry } from "../Components/Registry"; 4 | import { MustardConstanst } from "../Components/Constants"; 5 | import { DecoratedClassFieldsNormalizer } from "../Components/DecoratedFieldsNormalizer"; 6 | import { MustardUtils } from "../Components/Utils"; 7 | 8 | import { BuiltInCommands } from "./BuiltInCommands"; 9 | 10 | import { CommandNotFoundError } from "../Errors/CommandNotFoundError"; 11 | import { NoRootHandlerError } from "../Errors/NoRootHandlerError"; 12 | 13 | import type { Arguments } from "yargs-parser"; 14 | import type { 15 | CommandInput, 16 | CommandRegistryPayload, 17 | CommandStruct, 18 | } from "../Typings/Command.struct"; 19 | import type { 20 | CLIInstantiationConfiguration, 21 | CommandList, 22 | } from "../Typings/Configuration.struct"; 23 | import type { Provider } from "../Typings/DIService.struct"; 24 | import type { MaybeArray } from "../Typings/Shared.struct"; 25 | 26 | const debug = _debug("mustard:command-line"); 27 | 28 | export class CLI { 29 | constructor( 30 | readonly identifier: string, 31 | Commands: CommandList, 32 | private options?: CLIInstantiationConfiguration 33 | ) { 34 | this.initialize(Commands); 35 | } 36 | 37 | private parsedArgs!: Arguments; 38 | 39 | private initialize(Commands: CommandList) { 40 | this.normalizeConfigurations(); 41 | this.registerCommand(Commands); 42 | this.registerProvider(this.options?.providers ?? []); 43 | } 44 | 45 | public registerProvider(providers: MaybeArray) { 46 | const providerList = MustardUtils.ensureArray(providers); 47 | 48 | if (!providerList.length) return; 49 | 50 | providerList.forEach((provider) => { 51 | MustardUtils.isConstructable(provider) 52 | ? MustardRegistry.ExternalProviderRegistry.set(provider.name, provider) 53 | : MustardRegistry.ExternalProviderRegistry.set( 54 | provider.identifier, 55 | provider.value 56 | ); 57 | }); 58 | } 59 | 60 | private normalizeConfigurations() { 61 | const { 62 | allowUnknownOptions = false, 63 | enableUsage = true, 64 | enableVersion = false, 65 | lifeCycles = {}, 66 | didYouMean = true, 67 | ignoreValidationErrors = false, 68 | } = this.options ?? {}; 69 | 70 | this.options = { 71 | allowUnknownOptions, 72 | enableVersion, 73 | lifeCycles, 74 | didYouMean, 75 | enableUsage, 76 | ignoreValidationErrors, 77 | }; 78 | 79 | debug("normalized configurations: %O", this.options); 80 | } 81 | 82 | public configure(overrides: Partial) { 83 | debug("overriding configurations: %O", overrides); 84 | Object.assign(this.options ?? {}, overrides ?? {}); 85 | } 86 | 87 | public registerCommand(Commands: CommandList) { 88 | for (const Command of Commands) { 89 | const CommandRegistration = MustardRegistry.provideInit(Command.name); 90 | 91 | MustardRegistry.register( 92 | CommandRegistration.root 93 | ? MustardConstanst.RootCommandRegistryKey 94 | : CommandRegistration.commandInvokeName, 95 | 96 | CommandRegistration 97 | ); 98 | 99 | CommandRegistration.commandAlias 100 | ? MustardRegistry.register( 101 | CommandRegistration.commandAlias, 102 | CommandRegistration 103 | ) 104 | : void 0; 105 | 106 | if (CommandRegistration.childCommandList.length > 0) { 107 | this.registerCommand(CommandRegistration.childCommandList); 108 | } 109 | } 110 | } 111 | 112 | private instantiateWithParse() { 113 | MustardRegistry.provide().forEach((commandRegistration, key) => { 114 | const instance = new commandRegistration.Class(); 115 | 116 | const decoratedInstanceFields = 117 | MustardUtils.filterDecoratedInstanceFields(instance); 118 | 119 | MustardRegistry.upsert(key, { instance, decoratedInstanceFields }); 120 | }); 121 | 122 | this.parsedArgs = MustardUtils.parseFromProcessArgs( 123 | Array.from(MustardRegistry.VariadicOptions), 124 | MustardRegistry.OptionAliasMap 125 | ); 126 | 127 | debug("parsed arguments: %O", this.parsedArgs); 128 | } 129 | 130 | public start() { 131 | this.options?.lifeCycles?.onStart?.(); 132 | 133 | this.instantiateWithParse(); 134 | 135 | BuiltInCommands.useVersionCommand( 136 | this.parsedArgs, 137 | this.options?.enableVersion 138 | ); 139 | 140 | const useRootHandle = this.parsedArgs._?.length === 0; 141 | 142 | useRootHandle ? this.dispatchRootHandler() : this.dispatchCommand(); 143 | } 144 | 145 | private dispatchCommand() { 146 | const { command: commandRegistration, inputs: commandInput } = 147 | MustardUtils.findHandlerCommandWithInputs( 148 | this.parsedArgs._ 149 | ); 150 | 151 | // should only throw when no matched command found 152 | if (!commandRegistration) { 153 | throw new CommandNotFoundError(this.parsedArgs); 154 | } 155 | 156 | // execute command with help flag 157 | BuiltInCommands.useHelpCommand( 158 | this.identifier, 159 | this.parsedArgs, 160 | commandRegistration, 161 | this.options?.enableUsage 162 | ); 163 | 164 | this.handleCommandExecution(commandRegistration, commandInput); 165 | } 166 | 167 | private handleCommandExecution( 168 | commandRegistration: CommandRegistryPayload, 169 | commandInput: string[] 170 | ) { 171 | this.executeCommandFromRegistration(commandRegistration, commandInput) 172 | .then(this.options?.lifeCycles?.onComplete ?? (() => {})) 173 | .catch( 174 | this.options?.lifeCycles?.onError ?? 175 | ((err) => { 176 | throw err; 177 | }) 178 | ); 179 | } 180 | 181 | private async executeCommandFromRegistration( 182 | command: CommandRegistryPayload, 183 | inputs: string[] = [] 184 | ) { 185 | const handler: CommandStruct = command.instance!; 186 | 187 | this.options?.allowUnknownOptions === false 188 | ? DecoratedClassFieldsNormalizer.throwOnUnknownOptions( 189 | handler, 190 | this.parsedArgs, 191 | this.options?.didYouMean ?? true 192 | ) 193 | : void 0; 194 | 195 | DecoratedClassFieldsNormalizer.normalizeDecoratedFields( 196 | command, 197 | inputs, 198 | this.parsedArgs 199 | ); 200 | 201 | await handler.run(); 202 | } 203 | 204 | private dispatchRootHandler() { 205 | const rootCommandRegistration = MustardRegistry.provideRootCommand(); 206 | 207 | if (rootCommandRegistration) { 208 | // bin --help with root command specified 209 | // print help info for root command only(even there're other commands) 210 | BuiltInCommands.useHelpCommand( 211 | this.identifier, 212 | this.parsedArgs, 213 | rootCommandRegistration, 214 | this.options?.enableUsage 215 | ); 216 | 217 | this.executeCommandFromRegistration(rootCommandRegistration); 218 | } else if (this.options?.enableUsage) { 219 | // bin --help without root command specified 220 | // print help info for cpmplete app 221 | BuiltInCommands.useHelpCommand( 222 | this.identifier, 223 | true, 224 | undefined, 225 | this.options?.enableUsage 226 | ); 227 | } else { 228 | // no root command specified and options.enableUsage is disabled 229 | throw new NoRootHandlerError(); 230 | } 231 | } 232 | } 233 | --------------------------------------------------------------------------------