├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ ├── regression.yml │ └── bug_report.yml ├── actions │ ├── setup │ │ └── action.yml │ └── lint-build-test │ │ └── action.yml └── workflows │ ├── release.yml │ ├── docs-deploy.yml │ └── ci.yml ├── apps └── docs │ ├── public │ ├── CNAME │ ├── favicon.ico │ ├── img │ │ └── nest-commander.png │ └── make-scrollable-code-focusable.js │ ├── .astro │ ├── content-assets.mjs │ ├── content-modules.mjs │ └── types.d.ts │ ├── src │ ├── env.d.ts │ ├── pages │ │ ├── index.astro │ │ └── en │ │ │ ├── introduction │ │ │ ├── installation.md │ │ │ └── intro.md │ │ │ ├── schematics │ │ │ ├── installation.md │ │ │ └── usage.md │ │ │ ├── testing │ │ │ ├── installation.md │ │ │ └── factory.md │ │ │ └── features │ │ │ ├── utility.md │ │ │ ├── plugins.md │ │ │ └── factory.md │ ├── components │ │ ├── Footer │ │ │ └── Footer.astro │ │ ├── Header │ │ │ ├── SkipToContent.astro │ │ │ ├── SidebarToggle.tsx │ │ │ ├── LanguageSelect.css │ │ │ ├── AstroLogo.astro │ │ │ ├── LanguageSelect.tsx │ │ │ ├── Search.css │ │ │ ├── Search.tsx │ │ │ └── Header.astro │ │ ├── RightSidebar │ │ │ ├── RightSidebar.astro │ │ │ ├── ThemeToggleButton.css │ │ │ ├── TableOfContents.tsx │ │ │ ├── MoreMenu.astro │ │ │ └── ThemeToggleButton.tsx │ │ ├── PageContent │ │ │ └── PageContent.astro │ │ ├── HeadCommon.astro │ │ ├── HeadSEO.astro │ │ └── LeftSidebar │ │ │ └── LeftSidebar.astro │ ├── languages.ts │ ├── config.ts │ └── layouts │ │ └── MainLayout.astro │ ├── tsconfig.json │ ├── project.json │ └── astro.config.mjs ├── pnpm-workspace.yaml ├── .vscode └── settings.json ├── config ├── tsconfig.json ├── .czrc ├── renovate.json ├── integration ├── plugins │ ├── custom-name.json │ ├── nest-commander.json │ ├── src │ │ ├── plugin │ │ │ ├── index.ts │ │ │ ├── plugin.module.ts │ │ │ └── plugin.command.ts │ │ ├── foo.module.ts │ │ └── foo.command.ts │ └── test │ │ └── plugin.command.spec.ts ├── with-questions │ ├── src │ │ ├── hello.interface.ts │ │ ├── who.question.ts │ │ ├── root.module.ts │ │ └── hello.command.ts │ └── test │ │ └── hello.command.spec.ts ├── version-option │ ├── src │ │ └── root.module.ts │ └── test │ │ └── version.option.spec.ts ├── common │ └── log.service.ts ├── dot-command │ ├── src │ │ ├── dot.module.ts │ │ └── dot.command.ts │ └── test │ │ └── dot.command.spec.ts ├── output-config │ ├── src │ │ ├── root.module.ts │ │ └── basic.command.ts │ └── test │ │ └── output.config.spec.ts ├── root-command │ ├── src │ │ ├── root-command.module.ts │ │ ├── main.ts │ │ └── root.command.ts │ └── test │ │ └── root-command.spec.ts ├── basic │ ├── src │ │ ├── main.ts │ │ ├── root.module.ts │ │ ├── main-error-handler.ts │ │ └── basic.command.ts │ └── test │ │ ├── utils.ts │ │ └── basic.command.factory.spec.ts ├── option-choices │ ├── src │ │ ├── choices-provider.service.ts │ │ ├── option-choices.module.ts │ │ └── option-choices.command.ts │ └── test │ │ └── option-choices.spec.ts ├── pizza │ ├── src │ │ ├── pizza.interface.ts │ │ ├── pizza.module.ts │ │ ├── pizza.command.ts │ │ └── pizza.question.ts │ └── test │ │ └── pizza.command.spec.ts ├── register-provider │ ├── src │ │ ├── nested.module.ts │ │ ├── bottom.command.ts │ │ ├── mid-2.command.ts │ │ ├── mid-1.command.ts │ │ └── top.command.ts │ └── test │ │ └── register-with-subcommands.spec.ts ├── this-command │ ├── src │ │ ├── this-command.module.ts │ │ └── this-command.command.ts │ └── test │ │ └── this-command.command.spec.ts ├── this-handler │ ├── src │ │ ├── this-handler.module.ts │ │ └── this-handler.command.ts │ └── test │ │ └── this-handler.command.spec.ts ├── multiple │ ├── src │ │ ├── root.module.ts │ │ ├── foo.command.ts │ │ └── bar.command.ts │ └── test │ │ └── multiple.command.spec.ts ├── help-tests │ └── src │ │ ├── before.command.ts │ │ ├── after.command.ts │ │ ├── before-all.command.ts │ │ ├── after-all.command.ts │ │ ├── before-after.command.ts │ │ ├── after-after-all.command.ts │ │ ├── before-before-all.command.ts │ │ ├── foo.command.ts │ │ └── foo.module.ts ├── request-provider-override │ ├── src │ │ ├── request-scoped.service.ts │ │ ├── request-provider.module.ts │ │ └── simple.command.ts │ └── test │ │ └── index.spec.ts ├── sub-commands │ ├── src │ │ ├── bottom.command.ts │ │ ├── mid-2.command.ts │ │ ├── nested.module.ts │ │ ├── mid-1.command.ts │ │ └── top.command.ts │ └── test │ │ └── sub-commands.spec.ts ├── default-sub-commands │ ├── src │ │ ├── mid-2.command.ts │ │ ├── bottom.command.ts │ │ ├── nested.module.ts │ │ ├── mid-1.command.ts │ │ └── top.command.ts │ └── test │ │ └── default-sub-commands.spec.ts ├── project.json └── index.spec.ts ├── packages ├── nest-commander-testing │ ├── src │ │ ├── index.ts │ │ └── command-test.factory.ts │ ├── tsconfig.build.json │ ├── project.json │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── nest-commander │ ├── src │ │ ├── command-root.module.ts │ │ ├── index.ts │ │ ├── request-module.decorator.ts │ │ ├── completion.factory.interface.ts │ │ ├── cli-utility.service.ts │ │ ├── command-factory.interface.ts │ │ ├── command-runner.module.ts │ │ ├── command-runner.interface.ts │ │ └── command.decorators.ts │ ├── tsconfig.build.json │ ├── project.json │ └── package.json └── nest-commander-schematics │ ├── jest.config.js │ ├── tsconfig.build.json │ ├── src │ ├── question │ │ ├── question-options.interface.ts │ │ ├── files │ │ │ └── __name@dasherize__.questions.ts.template │ │ ├── index.ts │ │ └── schema.json │ ├── command │ │ ├── command-options.interface.ts │ │ ├── files │ │ │ ├── without-questions │ │ │ │ └── __name@dasherize__.command.ts.template │ │ │ └── with-questions │ │ │ │ └── __name@dasherize__.command.ts.template │ │ ├── index.ts │ │ └── schema.json │ ├── common │ │ ├── common-options.interface.ts │ │ └── index.ts │ └── collection.json │ ├── README.md │ ├── project.json │ ├── package.json │ └── CHANGELOG.md ├── tsconfig.build.json ├── commitlint.config.js ├── .npmrc ├── .c8rc ├── .lintstagedrc ├── .gitignore ├── tools └── schematics-postbuild ├── jest.config.js ├── .prettierrc ├── .changeset ├── config.json └── README.md ├── jest.integration.config.js ├── .swcrc ├── tsconfig.base.json ├── LICENSE ├── .eslintrc.js ├── nx.json ├── package.json └── CONTRIBUTING.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jmcdo29] 2 | -------------------------------------------------------------------------------- /apps/docs/public/CNAME: -------------------------------------------------------------------------------- 1 | nest-commander.jaymcdoniel.dev -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' -------------------------------------------------------------------------------- /apps/docs/.astro/content-assets.mjs: -------------------------------------------------------------------------------- 1 | export default new Map(); -------------------------------------------------------------------------------- /apps/docs/.astro/content-modules.mjs: -------------------------------------------------------------------------------- 1 | export default new Map(); -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | this file intentionally left blank for code folding in VSCode -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./node_modules/cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | [ -n "$CI" ] && exit 0 2 | ./node_modules/.bin/lint-staged 3 | -------------------------------------------------------------------------------- /integration/plugins/custom-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["./src/plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /integration/plugins/nest-commander.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["./src/plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './command-test.factory'; 2 | -------------------------------------------------------------------------------- /packages/nest-commander/src/command-root.module.ts: -------------------------------------------------------------------------------- 1 | export class CommandRootModule {} 2 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["test"] 4 | } -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | [ -n "$CI" ] && exit 0 2 | ./node_modules/.bin/commitlint --edit $1 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | publish-branch = "main" 2 | message = "chore(publish): %s" 3 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["integration", "**/test/*"], 3 | "reporter": ["lcov", "text" 4 | ] 5 | } -------------------------------------------------------------------------------- /apps/docs/.astro/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/with-questions/src/hello.interface.ts: -------------------------------------------------------------------------------- 1 | export interface HelloOptions { 2 | name?: string; 3 | } 4 | -------------------------------------------------------------------------------- /apps/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmcdo29/nest-commander/HEAD/apps/docs/public/favicon.ico -------------------------------------------------------------------------------- /apps/docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /integration/plugins/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginModule } from './plugin.module'; 2 | 3 | export default PluginModule; 4 | -------------------------------------------------------------------------------- /apps/docs/public/img/nest-commander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmcdo29/nest-commander/HEAD/apps/docs/public/img/nest-commander.png -------------------------------------------------------------------------------- /packages/nest-commander-testing/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.build.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /integration/version-option/src/root.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({ 4 | providers: [], 5 | }) 6 | export class RootModule {} 7 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "skipLibCheck": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "prettier --write", 4 | "eslint --ext ts" 5 | ], 6 | "*.{md,html,json,js}": [ 7 | "prettier --write" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /apps/docs/public/make-scrollable-code-focusable.js: -------------------------------------------------------------------------------- 1 | Array.from(document.getElementsByTagName('pre')).forEach((element) => { 2 | element.setAttribute('tabindex', '0'); 3 | }); 4 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '../../jest.config.js', 3 | testMatch: ['./**/*.spec.ts'], 4 | collectCoverageFrom: ['src/**/*.ts'], 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ 4 | .env 5 | 6 | tags 7 | 8 | # Generated Docusaurus files 9 | .docusaurus/ 10 | .cache-loader/ 11 | 12 | .nx/cache 13 | .nx/workspace-data -------------------------------------------------------------------------------- /integration/common/log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class LogService { 5 | log(...args: any[]): void { 6 | console.log(...args); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/dot-command/src/dot.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DotCommand } from './dot.command'; 3 | 4 | @Module({ 5 | providers: [DotCommand], 6 | }) 7 | export class DotModule {} 8 | -------------------------------------------------------------------------------- /integration/output-config/src/root.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BasicCommand } from './basic.command'; 3 | 4 | @Module({ 5 | providers: [BasicCommand], 6 | }) 7 | export class RootModule {} 8 | -------------------------------------------------------------------------------- /packages/nest-commander/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.build.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "moduleResolution": "Node16", 6 | "module": "Node16" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/plugins/src/plugin/plugin.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PluginCommand } from './plugin.command'; 3 | 4 | @Module({ 5 | providers: [PluginCommand], 6 | }) 7 | export class PluginModule {} 8 | -------------------------------------------------------------------------------- /integration/root-command/src/root-command.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RootComamnd } from './root.command'; 3 | 4 | @Module({ 5 | providers: [RootComamnd], 6 | }) 7 | export class RootCommandModule {} 8 | -------------------------------------------------------------------------------- /integration/basic/src/main.ts: -------------------------------------------------------------------------------- 1 | import { CommandFactory } from 'nest-commander'; 2 | import { RootModule } from './root.module'; 3 | 4 | const bootstrap = async () => { 5 | await CommandFactory.run(RootModule); 6 | }; 7 | 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /integration/option-choices/src/choices-provider.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ChoicesProvider { 5 | getChoicesForChoicesOption(): string[] { 6 | return ['yes', 'no']; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["test"] 9 | } 10 | -------------------------------------------------------------------------------- /integration/root-command/src/main.ts: -------------------------------------------------------------------------------- 1 | import { CommandFactory } from 'nest-commander'; 2 | import { RootCommandModule } from './root-command.module'; 3 | 4 | const bootstrap = async () => { 5 | await CommandFactory.run(RootCommandModule); 6 | }; 7 | 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /integration/pizza/src/pizza.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PizzaOptions { 2 | toppings: string; 3 | toBeDelivered: boolean; 4 | phone: string; 5 | size: string; 6 | quantity: number; 7 | beverage: string; 8 | comments: string; 9 | prize?: string; 10 | } 11 | -------------------------------------------------------------------------------- /integration/plugins/src/plugin/plugin.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | @Command({ name: 'plug' }) 4 | export class PluginCommand extends CommandRunner { 5 | async run() { 6 | console.log('This is from the plugin!'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/plugins/src/foo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LogService } from '../../common/log.service'; 3 | import { FooCommand } from './foo.command'; 4 | 5 | @Module({ 6 | providers: [LogService, FooCommand], 7 | }) 8 | export class FooModule {} 9 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/question/question-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { CommonOptions } from '../common/common-options.interface'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 4 | export interface QuestionOptions extends CommonOptions {} 5 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/command/command-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { CommonOptions } from '../common/common-options.interface'; 2 | 3 | export interface CommandOptions extends CommonOptions { 4 | question: string; 5 | default: boolean; 6 | isDefault?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /tools/schematics-postbuild: -------------------------------------------------------------------------------- 1 | #! /usr/bin/sh 2 | 3 | packageName=nest-commander-schematics 4 | 5 | cd packages/$packageName/src 6 | cp --parents $(find -name "*.json") ../../../dist/$packageName/src 7 | cp --parents $(find -name "*.template") ../../../dist/$packageName/src 8 | cd ../../../ 9 | -------------------------------------------------------------------------------- /integration/basic/src/root.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BasicCommand } from './basic.command'; 3 | import { LogService } from './../../common/log.service'; 4 | 5 | @Module({ 6 | providers: [BasicCommand, LogService], 7 | }) 8 | export class RootModule {} 9 | -------------------------------------------------------------------------------- /integration/pizza/src/pizza.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PizzaCommand } from './pizza.command'; 3 | import { PizzaQuestion } from './pizza.question'; 4 | 5 | @Module({ 6 | providers: [PizzaCommand, PizzaQuestion], 7 | }) 8 | export class PizzaModule {} 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'ts'], 3 | rootDir: '.', 4 | testMatch: ['*.spec.ts'], 5 | transform: { 6 | '^.+\\.ts$': 'ts-jest', 7 | }, 8 | testEnvironment: 'node', 9 | collectCoverageFrom: ['packages/**/src/*.ts'], 10 | collectCoverage: true, 11 | }; 12 | -------------------------------------------------------------------------------- /integration/register-provider/src/nested.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { TopCommand } from './top.command'; 5 | 6 | @Module({ 7 | providers: [LogService, ...TopCommand.registerWithSubCommands()], 8 | }) 9 | export class NestedModule {} 10 | -------------------------------------------------------------------------------- /integration/this-command/src/this-command.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common/decorators'; 2 | import { LogService } from '../../common/log.service'; 3 | import { ThisCommandCommand } from './this-command.command'; 4 | 5 | @Module({ 6 | providers: [LogService, ThisCommandCommand], 7 | }) 8 | export class ThisCommandModule {} 9 | -------------------------------------------------------------------------------- /integration/this-handler/src/this-handler.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common/decorators'; 2 | import { LogService } from '../../common/log.service'; 3 | import { ThisHandlerCommand } from './this-handler.command'; 4 | 5 | @Module({ 6 | providers: [LogService, ThisHandlerCommand], 7 | }) 8 | export class ThisHandlerModule {} 9 | -------------------------------------------------------------------------------- /integration/with-questions/src/who.question.ts: -------------------------------------------------------------------------------- 1 | import { Question, QuestionSet } from 'nest-commander'; 2 | 3 | @QuestionSet({ name: 'hello' }) 4 | export class WhoQuestion { 5 | @Question({ 6 | message: 'What is your name?', 7 | name: 'name', 8 | }) 9 | parseName(val: string) { 10 | return val; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | ## To encourage contributors to use issue templates, we don't allow blank issues 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: "\u2753 Our GitHub Discussions page" 6 | url: "https://github.com/jmcdo29/nest-commander/discussions" 7 | about: "Please ask and answer questions here!" 8 | -------------------------------------------------------------------------------- /integration/dot-command/src/dot.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | @Command({ name: 'dot', options: { isDefault: true } }) 4 | export class DotCommand extends CommandRunner { 5 | async run() { 6 | if (!this.command) { 7 | throw new Error('No .command property set'); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always", 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "overrides": [ 7 | { 8 | "files": "*.md", 9 | "options": { 10 | "useTabs": false, 11 | "trailingComma": "none", 12 | "proseWrap": "always" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/common/common-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Path } from '@angular-devkit/core'; 2 | 3 | export interface CommonOptions { 4 | name: string; 5 | spec: boolean; 6 | flat: boolean; 7 | sourceRoot: string; 8 | path?: string | Path; 9 | type?: string; 10 | metadata?: string; 11 | module?: Path | null; 12 | } 13 | -------------------------------------------------------------------------------- /integration/multiple/src/root.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BarCommand } from './bar.command'; 3 | import { FooCommand } from './foo.command'; 4 | import { LogService } from './../../common/log.service'; 5 | 6 | @Module({ 7 | providers: [BarCommand, FooCommand, LogService], 8 | }) 9 | export class MultipleCommandModule {} 10 | -------------------------------------------------------------------------------- /integration/help-tests/src/before.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ name: 'before', description: 'before' }) 4 | export class BeforeCommand extends CommandRunner { 5 | async run() { 6 | /* no op */ 7 | } 8 | 9 | @Help('before') 10 | beforeHelp() { 11 | return 'before help'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/with-questions/src/root.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LogService } from '../../common/log.service'; 3 | import { HelloCommand } from './hello.command'; 4 | import { WhoQuestion } from './who.question'; 5 | 6 | @Module({ 7 | providers: [HelloCommand, WhoQuestion, LogService], 8 | }) 9 | export class HelloCommandModule {} 10 | -------------------------------------------------------------------------------- /integration/basic/src/main-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { CommandFactory } from 'nest-commander'; 2 | import { RootModule } from './root.module'; 3 | 4 | const bootstrap = async () => { 5 | await CommandFactory.run(RootModule, { 6 | errorHandler: (err) => { 7 | console.log(err.message); 8 | process.exit(0); 9 | }, 10 | }); 11 | }; 12 | 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/question/files/__name@dasherize__.questions.ts.template: -------------------------------------------------------------------------------- 1 | import { QuestionSet, Question } from 'nest-commander'; 2 | 3 | @QuestionSet({ name: '<%= name %>' }) 4 | export class <%= classify(name) %>Questions { 5 | @Question({ 6 | name: 'default', 7 | }) 8 | parseDefault(str: string): string { 9 | return str; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /integration/help-tests/src/after.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ 4 | name: 'after', 5 | description: 'after', 6 | }) 7 | export class AfterCommand extends CommandRunner { 8 | async run() { 9 | /* no op */ 10 | } 11 | 12 | @Help('after') 13 | afterHelp() { 14 | return 'after help'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integration/help-tests/src/before-all.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ name: 'before-all', description: 'before-all' }) 4 | export class BeforeAllCommand extends CommandRunner { 5 | async run() { 6 | /* no op */ 7 | } 8 | 9 | @Help('beforeAll') 10 | beforeAllHelp() { 11 | return 'before all help'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/help-tests/src/after-all.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ 4 | name: 'after-all', 5 | description: 'after all', 6 | }) 7 | export class AfterAllCommand extends CommandRunner { 8 | async run() { 9 | /* no op */ 10 | } 11 | 12 | @Help('afterAll') 13 | afterAllHelp() { 14 | return 'after all help'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/docs/src/components/Footer/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AvatarList from './AvatarList.astro'; 3 | type Props = { 4 | path: string; 5 | }; 6 | const { path } = Astro.props as Props; 7 | --- 8 | 9 |
10 | 11 |
12 | 13 | 20 | -------------------------------------------------------------------------------- /integration/request-provider-override/src/request-scoped.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { REQUEST } from '@nestjs/core'; 3 | 4 | @Injectable() 5 | export class RequestScopedService { 6 | constructor(@Inject(REQUEST) private readonly req: Record) { 7 | console.log(this.req); 8 | } 9 | 10 | getRequest() { 11 | return this.req; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/option-choices/src/option-choices.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LogService } from '../../common/log.service'; 3 | import { ChoicesProvider } from './choices-provider.service'; 4 | import { OptionsTestCommand } from './option-choices.command'; 5 | 6 | @Module({ 7 | providers: [LogService, ChoicesProvider, OptionsTestCommand], 8 | }) 9 | export class OptionChoicesModule {} 10 | -------------------------------------------------------------------------------- /integration/sub-commands/src/bottom.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'bottom' }) 6 | export class BottomCommand extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-1 bottom command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [], 10 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 11 | "onlyUpdatePeerDependentsWhenOutOfRange": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/register-provider/src/bottom.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'bottom' }) 6 | export class BottomCommand extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-1 bottom command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration/sub-commands/src/mid-2.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'mid-2', aliases: ['m'] }) 6 | export class Mid2Command extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-2 command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration/register-provider/src/mid-2.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'mid-2', aliases: ['m'] }) 6 | export class Mid2Command extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-2 command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/command/files/without-questions/__name@dasherize__.command.ts.template: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | @Command({ name: '<%= lowercase(name) %>', options: { isDefault: <%= (isDefault) %> } }) 4 | export class <%= classify(name) %>Command extends CommandRunner { 5 | async run(inputs: string[], options: Record) { 6 | console.log({ inputs, options }); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/default-sub-commands/src/mid-2.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'mid-2', aliases: ['m'] }) 6 | export class Mid2Command extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-2 command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration/multiple/src/foo.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { LogService } from './../../common/log.service'; 3 | 4 | @Command({ name: 'foo', options: { isDefault: true } }) 5 | export class FooCommand extends CommandRunner { 6 | constructor(private readonly logService: LogService) { 7 | super(); 8 | } 9 | 10 | async run(): Promise { 11 | this.logService.log('foo'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/multiple/src/bar.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { LogService } from './../../common/log.service'; 3 | 4 | @Command({ name: 'bar', options: { isDefault: false } }) 5 | export class BarCommand extends CommandRunner { 6 | constructor(private readonly logService: LogService) { 7 | super(); 8 | } 9 | 10 | async run(): Promise { 11 | this.logService.log('bar'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration/plugins/src/foo.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | 4 | @Command({ 5 | name: 'phooey', 6 | description: 'This is a phooey command.', 7 | }) 8 | export class FooCommand extends CommandRunner { 9 | constructor(private readonly log: LogService) { 10 | super(); 11 | } 12 | 13 | async run() { 14 | this.log.log('Foo!'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/docs/src/languages.ts: -------------------------------------------------------------------------------- 1 | import { KNOWN_LANGUAGE_CODES, KNOWN_LANGUAGES } from './config'; 2 | export { KNOWN_LANGUAGE_CODES, KNOWN_LANGUAGES }; 3 | 4 | export const langPathRegex = /\/([a-z]{2}-?[A-Z]{0,2})\//; 5 | 6 | export function getLanguageFromURL(pathname: string) { 7 | const langCodeMatch = pathname.match(langPathRegex); 8 | const langCode = langCodeMatch ? langCodeMatch[1] : 'en'; 9 | return langCode as (typeof KNOWN_LANGUAGE_CODES)[number]; 10 | } 11 | -------------------------------------------------------------------------------- /integration/default-sub-commands/src/bottom.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | 5 | @SubCommand({ name: 'bottom', options: { isDefault: true } }) 6 | export class BottomCommand extends CommandRunner { 7 | constructor(private readonly log: LogService) { 8 | super(); 9 | } 10 | 11 | async run() { 12 | this.log.log('top mid-1 bottom command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration/request-provider-override/src/request-provider.module.ts: -------------------------------------------------------------------------------- 1 | import { RequestModule } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | import { RequestScopedService } from './request-scoped.service'; 4 | import { SimpleCommand } from './simple.command'; 5 | 6 | @RequestModule({ 7 | providers: [LogService, SimpleCommand, RequestScopedService], 8 | requestObject: { custom: 'value' }, 9 | }) 10 | export class RequestProviderModule {} 11 | -------------------------------------------------------------------------------- /integration/basic/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export type ExpectedParam = 4 | | Record<'string', string> 5 | | Record<'number', number> 6 | | Record<'boolean', boolean>; 7 | 8 | const [firstArg] = process.argv; 9 | // overwrite the second arg to make commander happy 10 | const secondArg = join(__dirname, 'basic.command.js'); 11 | 12 | export function setArgv(...args: string[]) { 13 | process.argv = [firstArg, secondArg, 'basic', 'test', ...args]; 14 | } 15 | -------------------------------------------------------------------------------- /integration/help-tests/src/before-after.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ 4 | name: 'before-after', 5 | description: 'before-after', 6 | }) 7 | export class BeforeAfterCommand extends CommandRunner { 8 | async run() { 9 | /* no op */ 10 | } 11 | 12 | @Help('before') 13 | beforeHelp() { 14 | return 'before help'; 15 | } 16 | 17 | @Help('after') 18 | afterHelp() { 19 | return 'after help'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration", 3 | "$schema": "../node_modules/nx/schemas/project-schema.json", 4 | "type": "library", 5 | "targets": { 6 | "e2e": { 7 | "executor": "nx-uvu:uvu", 8 | "options": { 9 | "rootDir": "integration", 10 | "useSwc": true, 11 | "coverage": true, 12 | "pattern": "index.spec.ts" 13 | } 14 | } 15 | }, 16 | "implicitDependencies": ["nest-commander", "nest-commander-testing"] 17 | } 18 | -------------------------------------------------------------------------------- /integration/sub-commands/src/nested.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { BottomCommand } from './bottom.command'; 5 | import { Mid1Command } from './mid-1.command'; 6 | import { Mid2Command } from './mid-2.command'; 7 | import { TopCommand } from './top.command'; 8 | 9 | @Module({ 10 | providers: [LogService, TopCommand, Mid1Command, Mid2Command, BottomCommand], 11 | }) 12 | export class NestedModule {} 13 | -------------------------------------------------------------------------------- /integration/default-sub-commands/src/nested.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { BottomCommand } from './bottom.command'; 5 | import { Mid1Command } from './mid-1.command'; 6 | import { Mid2Command } from './mid-2.command'; 7 | import { TopCommand } from './top.command'; 8 | 9 | @Module({ 10 | providers: [LogService, TopCommand, Mid1Command, Mid2Command, BottomCommand], 11 | }) 12 | export class NestedModule {} 13 | -------------------------------------------------------------------------------- /integration/sub-commands/src/mid-1.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { BottomCommand } from './bottom.command'; 5 | 6 | @SubCommand({ name: 'mid-1', subCommands: [BottomCommand] }) 7 | export class Mid1Command extends CommandRunner { 8 | constructor(private readonly log: LogService) { 9 | super(); 10 | } 11 | 12 | async run() { 13 | this.log.log('top mid-1 command'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration/help-tests/src/after-after-all.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ 4 | name: 'after-after-all', 5 | description: 'after after all', 6 | }) 7 | export class AfterAfterAllCommand extends CommandRunner { 8 | async run() { 9 | /* no op */ 10 | } 11 | 12 | @Help('after') 13 | afterHelp() { 14 | return 'after help'; 15 | } 16 | 17 | @Help('afterAll') 18 | afterAllHelp() { 19 | return 'after all help'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration/register-provider/src/mid-1.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { BottomCommand } from './bottom.command'; 5 | 6 | @SubCommand({ name: 'mid-1', subCommands: [BottomCommand] }) 7 | export class Mid1Command extends CommandRunner { 8 | constructor(private readonly log: LogService) { 9 | super(); 10 | } 11 | 12 | async run() { 13 | this.log.log('top mid-1 command'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration/help-tests/src/before-before-all.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | 3 | @Command({ 4 | name: 'before-before-all', 5 | description: 'before-before-all', 6 | }) 7 | export class BeforeBeforeAllCommand extends CommandRunner { 8 | async run() { 9 | /* no op */ 10 | } 11 | 12 | @Help('before') 13 | beforeHelp() { 14 | return 'before help'; 15 | } 16 | 17 | @Help('beforeAll') 18 | beforeAllHelp() { 19 | return 'before all help'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) 4 | 5 | We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 6 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/question/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from '@angular-devkit/schematics'; 2 | import { CommonSchematicFactory } from '../common'; 3 | import { QuestionOptions } from './question-options.interface'; 4 | 5 | class QuestionSchematicsFactory extends CommonSchematicFactory { 6 | type = 'questions'; 7 | } 8 | 9 | export function question(options: QuestionOptions): Rule { 10 | const questionFactory = new QuestionSchematicsFactory(); 11 | return questionFactory.create(options); 12 | } 13 | -------------------------------------------------------------------------------- /packages/nest-commander/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cli-utility.service'; 2 | export * from './command.decorators'; 3 | export * from './command.factory'; 4 | export * from './completion.factory'; 5 | export * from './command-runner.interface'; 6 | export * from './command-runner.module'; 7 | export * from './command-runner.service'; 8 | export { Inquirer } from './constants'; 9 | export { CommanderOptionsType } from './command-factory.interface'; 10 | export * from './inquirer.service'; 11 | export * from './request-module.decorator'; 12 | -------------------------------------------------------------------------------- /integration/output-config/src/basic.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Option } from 'nest-commander'; 2 | 3 | @Command({ name: 'basic' }) 4 | export class BasicCommand extends CommandRunner { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | async run(): Promise { 10 | // no op 11 | } 12 | 13 | @Option({ 14 | flags: '-n, --number ', 15 | description: 'A basic number option', 16 | required: true, 17 | }) 18 | parseNumber(val: string): number { 19 | return Number(val); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/nest-commander/src/request-module.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Module, ModuleMetadata } from '@nestjs/common'; 2 | import { REQUEST } from '@nestjs/core'; 3 | 4 | export const RequestModule = ( 5 | metadata: ModuleMetadata & { requestObject: Record }, 6 | ): ClassDecorator => { 7 | const { requestObject, ...moduleMetadata } = metadata; 8 | moduleMetadata.providers ??= []; 9 | moduleMetadata.providers.push({ 10 | provide: REQUEST, 11 | useValue: requestObject, 12 | }); 13 | return Module(moduleMetadata); 14 | }; 15 | -------------------------------------------------------------------------------- /integration/this-command/src/this-command.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | 4 | @Command({ name: 'this-command', arguments: '' }) 5 | export class ThisCommandCommand extends CommandRunner { 6 | constructor(private readonly log: LogService) { 7 | super(); 8 | } 9 | 10 | async run(params: string[]) { 11 | this.logHandler(params); 12 | } 13 | 14 | logHandler(...args: any[]): void { 15 | this.log.log(...args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "command": { 5 | "description": "A schematic to generate a command file", 6 | "factory": "./command/index#command", 7 | "schema": "./command/schema.json" 8 | }, 9 | "question": { 10 | "description": "A schematic to generate a question file", 11 | "factory": "./question/index#question", 12 | "schema": "./question/schema.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration/default-sub-commands/src/mid-1.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, SubCommand } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { BottomCommand } from './bottom.command'; 5 | 6 | @SubCommand({ 7 | name: 'mid-1', 8 | subCommands: [BottomCommand], 9 | options: { isDefault: true }, 10 | }) 11 | export class Mid1Command extends CommandRunner { 12 | constructor(private readonly log: LogService) { 13 | super(); 14 | } 15 | 16 | async run() { 17 | this.log.log('top mid-1 command'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration/pizza/src/pizza.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, InquirerService } from 'nest-commander'; 2 | import { PizzaOptions } from './pizza.interface'; 3 | 4 | @Command({ name: 'pizza', options: { isDefault: true } }) 5 | export class PizzaCommand extends CommandRunner { 6 | constructor(private readonly inquirerService: InquirerService) { 7 | super(); 8 | } 9 | async run(_inputs: string[], options?: PizzaOptions): Promise { 10 | options = await this.inquirerService.ask('pizza', options); 11 | console.log(options); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /jest.integration.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./jest.config'); 2 | module.exports = { 3 | ...baseConfig, 4 | testMatch: ['/integration/**/*.spec.ts'], 5 | collectCoverage: true, 6 | verbose: true, 7 | globals: { 8 | 'ts-jest': { 9 | tsconfig: 'tsconfig.json', 10 | }, 11 | }, 12 | moduleNameMapper: { 13 | '^nest-commander$': ['/packages/nest-commander/src'], 14 | '^nest-commander-testing$': ['/packages/nest-commander-testing/src'], 15 | }, 16 | modulePathIgnorePatterns: ['/dist/'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/nest-commander/src/completion.factory.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CompletionFactoryOptions { 2 | /** 3 | * @description Your CLI command name 4 | */ 5 | cmd: string; 6 | 7 | /** 8 | * @description Support Fig completion out of the box 9 | * @default false 10 | */ 11 | fig?: boolean; 12 | 13 | /** 14 | * @description Use native shell completion with `tab` 15 | * @default false 16 | */ 17 | nativeShell?: false | NativeShellConfiguration; 18 | } 19 | 20 | export interface NativeShellConfiguration { 21 | executablePath: string; 22 | } 23 | -------------------------------------------------------------------------------- /integration/root-command/src/root.command.ts: -------------------------------------------------------------------------------- 1 | import { CommandRunner, Option, RootCommand } from 'nest-commander'; 2 | 3 | @RootCommand({ options: { isDefault: true } }) 4 | export class RootComamnd extends CommandRunner { 5 | async run(): Promise { 6 | // no op 7 | } 8 | 9 | @Option({ 10 | name: 'useYellow', 11 | flags: '-y, --yellow ', 12 | }) 13 | parseYellow(yellow: string) { 14 | return yellow; 15 | } 16 | 17 | @Option({ 18 | flags: '-b, --blue ', 19 | }) 20 | parseBlue(blue: string) { 21 | return blue; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "decorators": true 6 | }, 7 | "transform": { 8 | "legacyDecorator": true, 9 | "decoratorMetadata": true 10 | }, 11 | "target": "es2017", 12 | "keepClassNames": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "nest-commander": ["packages/nest-commander/src/index.ts"], 16 | "nest-commander-testing": ["packages/nest-commander-testing/src/index.ts"] 17 | } 18 | }, 19 | "module": { 20 | "type": "commonjs", 21 | "strictMode": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/docs/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/docs/src", 6 | "targets": { 7 | "build": { 8 | "executor": "nx:run-commands", 9 | "options": { 10 | "command": "pnpm astro build --root apps/docs" 11 | } 12 | }, 13 | "serve": { 14 | "executor": "nx:run-commands", 15 | "options": { 16 | "command": "pnpm astro dev --port 3333 --root apps/docs --verbose" 17 | } 18 | } 19 | }, 20 | "tags": [] 21 | } 22 | -------------------------------------------------------------------------------- /integration/sub-commands/src/top.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { Mid1Command } from './mid-1.command'; 5 | import { Mid2Command } from './mid-2.command'; 6 | 7 | @Command({ 8 | name: 'top', 9 | arguments: '[name]', 10 | subCommands: [Mid1Command, Mid2Command], 11 | }) 12 | export class TopCommand extends CommandRunner { 13 | constructor(private readonly log: LogService) { 14 | super(); 15 | } 16 | 17 | async run(inputs: string[]) { 18 | this.log.log('top command'); 19 | this.log.log(inputs); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration/default-sub-commands/src/top.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { Mid1Command } from './mid-1.command'; 5 | import { Mid2Command } from './mid-2.command'; 6 | 7 | @Command({ 8 | name: 'top', 9 | arguments: '[name]', 10 | subCommands: [Mid1Command, Mid2Command], 11 | }) 12 | export class TopCommand extends CommandRunner { 13 | constructor(private readonly log: LogService) { 14 | super(); 15 | } 16 | 17 | async run(inputs: string[]) { 18 | this.log.log('top command'); 19 | this.log.log(inputs); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration/register-provider/src/top.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | 3 | import { LogService } from '../../common/log.service'; 4 | import { Mid1Command } from './mid-1.command'; 5 | import { Mid2Command } from './mid-2.command'; 6 | 7 | @Command({ 8 | name: 'top', 9 | arguments: '[name]', 10 | subCommands: [Mid1Command, Mid2Command], 11 | }) 12 | export class TopCommand extends CommandRunner { 13 | constructor(private readonly log: LogService) { 14 | super(); 15 | } 16 | 17 | async run(inputs: string[]) { 18 | this.log.log('top command'); 19 | this.log.log(inputs); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/command/files/with-questions/__name@dasherize__.command.ts.template: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, InquirerService } from 'nest-commander'; 2 | 3 | @Command({ name: '<%= lowercase(name) %>', options: { isDefault: <%= (isDefault) %> } }) 4 | export class <%= classify(name) %>Command extends CommandRunner { 5 | constructor(private readonly inquirerService: InquirerService) { 6 | super() 7 | } 8 | 9 | async run(inputs: string[], options?: Record) { 10 | options = await this.inquirerService.prompt('<%= (question) %>', options); 11 | console.log({ inputs, options }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/introduction/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | To get started with `nest-commander` you can either add it to a `nest new` project, created using 7 | the `@nestjs/cli`, or you can set the project up from scratch and make sure to install 8 | `nest-commander`, `@nestjs/common` and `@nestjs/core`. 9 | 10 | ```sh 11 | npm i nest-commander 12 | # OR 13 | yarn add nest-commander 14 | # OR 15 | pnpm i nest-commander 16 | ``` 17 | 18 | :::note 19 | 20 | Soon there will be a schematic for scaffolding an entire CLI application. Keep an eye out for it. 21 | 22 | ::: 23 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/SkipToContent.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = {}; 3 | --- 4 | 5 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "outDir": "./dist", 10 | "baseUrl": ".", 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "noUnusedLocals": false, 14 | "sourceMap": true, 15 | "lib": ["ES7"], 16 | "paths": { 17 | "nest-commander": ["packages/nest-commander/src/index.ts"], 18 | "nest-commander-testing": ["packages/nest-commander-testing/src/index.ts"] 19 | } 20 | }, 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/schematics/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | So now you know about this awesome package and want quick ways to set up commands and questions and 7 | tie them to each other. You could write your own shell script, or use handlebars and some clever 8 | generation of code, maybe even write your own schematic, but why go through all that trouble when 9 | one already exists? Simply install the `nest-commander-schematics` collection as a `devDependency` 10 | 11 | ```shell 12 | npm i -D nest-commander-schematics 13 | yarn add -D nest-commander-schematics 14 | pnpm i -D nest-commander-schematics 15 | ``` 16 | -------------------------------------------------------------------------------- /integration/request-provider-override/src/simple.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | import { RequestScopedService } from './request-scoped.service'; 4 | 5 | @Command({ name: 'simple', options: { isDefault: true } }) 6 | export class SimpleCommand extends CommandRunner { 7 | constructor( 8 | private readonly requestProvider: RequestScopedService, 9 | private readonly logger: LogService, 10 | ) { 11 | super(); 12 | } 13 | 14 | async run(_params: Record, _options: Record) { 15 | this.logger.log(this.requestProvider.getRequest()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /integration/dot-command/test/dot.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommandTestFactory } from 'nest-commander-testing'; 2 | import { suite } from 'uvu'; 3 | import { ok, unreachable } from 'uvu/assert'; 4 | import { DotModule } from '../src/dot.module'; 5 | 6 | export const DotCommandSuite = suite('DotCommand'); 7 | DotCommandSuite('Command does not throw error', async () => { 8 | try { 9 | const commandInstance = await CommandTestFactory.createTestingCommand({ 10 | imports: [DotModule], 11 | }).compile(); 12 | await CommandTestFactory.run(commandInstance); 13 | ok(true); 14 | } catch (err) { 15 | unreachable('If we got here the .command property was not populated'); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Set up action environment' 2 | description: 'Install Node, pnpm, and install deps with caching' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Install Node 7 | uses: actions/setup-node@v4 8 | with: 9 | node-version: 20.x 10 | 11 | - name: Cache pnpm modules 12 | uses: actions/cache@v4 13 | if: ${{ !contains(github.event.pull_request.user.login, 'bot') }} 14 | with: 15 | path: ~/.pnpm-store 16 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 17 | restore-keys: | 18 | ${{ runner.os }}- 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | run_install: true 25 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/command/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Source } from '@angular-devkit/schematics'; 2 | import { CommonSchematicFactory } from '../common'; 3 | import { CommandOptions } from './command-options.interface'; 4 | 5 | class CommandSchematicFactory extends CommonSchematicFactory { 6 | type = 'command'; 7 | 8 | generate(options: CommandOptions): Source { 9 | this.templatePath = options.question 10 | ? './files/with-questions' 11 | : './files/without-questions'; 12 | options.isDefault = options.default; 13 | return super.generate(options); 14 | } 15 | } 16 | 17 | export function command(options: CommandOptions): Rule { 18 | const commandFactory = new CommandSchematicFactory(); 19 | return commandFactory.create(options); 20 | } 21 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started With Schematics 2 | 3 | This repository is a basic Schematic implementation that serves as a starting point to create and 4 | publish Schematics to NPM. 5 | 6 | ### Testing 7 | 8 | To test locally, install `@angular-devkit/schematics-cli` globally and use the `schematics` command 9 | line tool. That tool acts the same as the `generate` command of the Angular CLI, but also has a 10 | debug mode. 11 | 12 | Check the documentation with 13 | 14 | ```bash 15 | schematics --help 16 | ``` 17 | 18 | ### Unit Testing 19 | 20 | `npm run test` will run the unit tests, using Jasmine as a runner and test framework. 21 | 22 | ### Publishing 23 | 24 | To publish, simply do: 25 | 26 | ```bash 27 | npm run build 28 | npm publish 29 | ``` 30 | 31 | That's it! 32 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/testing/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | So now you've got a fancy CLI application, and you've got it unit tested just fine, but you need to 7 | do e2e testing. How do you do it? With express there's supertest, and fastify has light-my-request 8 | with its `inject` framework, but how do you pass in CLI commands without spawning another shell? 9 | That's where the `nest-commander-testing` package comes in! It's built on top of Nest's own 10 | `@nestjs/testing` package, so the API should be very familiar. To install, simple use your favorite 11 | package manager and set the package as a `devDependency` 12 | 13 | ```shell 14 | npm i -D nest-commander-testing 15 | # OR 16 | yarn add -D nest-commander-testing 17 | # OR 18 | pnpm i -D nest-commander-testing 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/actions/lint-build-test/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run Lint, Build, Tests, and Coverage Collection' 2 | description: 'Run the required actions' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Run Linter 7 | run: pnpm lint 8 | shell: bash 9 | - name: Build Project 10 | run: pnpm build 11 | shell: bash 12 | - name: Run Integration Tests 13 | run: pnpm e2e --skip-nx-cache 14 | shell: bash 15 | env: 16 | NX_CLOUD_DISTRIBUTED_EXECUTION: false 17 | - name: Stop Nx Cloud Agents 18 | run: pnpx nx-cloud stop-all-agents 19 | shell: bash 20 | - name: Debug Coverage 21 | run: cat coverage/lcov.info 22 | shell: bash 23 | - name: Upload Coverage 24 | uses: actions/upload-artifact@master 25 | with: 26 | name: coverage 27 | path: coverage/lcov.info 28 | -------------------------------------------------------------------------------- /integration/help-tests/src/foo.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Help } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | 4 | @Command({ 5 | name: 'foo', 6 | options: { 7 | isDefault: true, 8 | }, 9 | }) 10 | export class FooCommand extends CommandRunner { 11 | constructor(private readonly logger: LogService) { 12 | super(); 13 | } 14 | async run(inputs: any, options: any) { 15 | this.logger.log(inputs, options); 16 | } 17 | 18 | @Help('before') 19 | beforeHelp() { 20 | return 'before help'; 21 | } 22 | 23 | @Help('after') 24 | afterHelp() { 25 | return 'after help'; 26 | } 27 | 28 | @Help('beforeAll') 29 | beforeAllHelp() { 30 | return 'before all help'; 31 | } 32 | 33 | @Help('afterAll') 34 | afterAllHelp() { 35 | return 'after all help'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/docs/src/components/RightSidebar/RightSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TableOfContents from './TableOfContents'; 3 | import MoreMenu from './MoreMenu.astro'; 4 | import type { MarkdownHeading } from 'astro'; 5 | 6 | type Props = { 7 | headings: MarkdownHeading[]; 8 | githubEditUrl: string; 9 | }; 10 | 11 | const { headings, githubEditUrl } = Astro.props as Props; 12 | --- 13 | 14 | 20 | 21 | 35 | -------------------------------------------------------------------------------- /apps/docs/src/components/RightSidebar/ThemeToggleButton.css: -------------------------------------------------------------------------------- 1 | .theme-toggle { 2 | display: inline-flex; 3 | align-items: center; 4 | gap: 0.25em; 5 | padding: 0.33em 0.67em; 6 | border-radius: 99em; 7 | background-color: var(--theme-code-inline-bg); 8 | } 9 | 10 | .theme-toggle > label:focus-within { 11 | outline: 2px solid transparent; 12 | box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white; 13 | } 14 | 15 | .theme-toggle > label { 16 | color: var(--theme-code-inline-text); 17 | position: relative; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | opacity: 0.5; 22 | } 23 | 24 | .theme-toggle .checked { 25 | color: var(--theme-accent); 26 | opacity: 1; 27 | } 28 | 29 | input[name='theme-toggle'] { 30 | position: absolute; 31 | opacity: 0; 32 | top: 0; 33 | right: 0; 34 | bottom: 0; 35 | left: 0; 36 | z-index: -1; 37 | } 38 | -------------------------------------------------------------------------------- /packages/nest-commander/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "type": "library", 5 | "sourceRoot": "packages/nest-commander/src", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "options": { 10 | "outputPath": "dist/nest-commander", 11 | "main": "packages/nest-commander/src/index.ts", 12 | "tsConfig": "packages/nest-commander/tsconfig.build.json", 13 | "deleteOutputPath": true, 14 | "packageJson": "packages/nest-commander/package.json", 15 | "assets": ["packages/nest-commander/*.md"] 16 | } 17 | }, 18 | "publish": { 19 | "executor": "nx:run-commands", 20 | "options": { 21 | "cwd": "dist/nest-commander", 22 | "command": "pnpm publish" 23 | } 24 | } 25 | }, 26 | "implicitDependencies": [] 27 | } 28 | -------------------------------------------------------------------------------- /integration/request-provider-override/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { stubMethod } from 'hanbi'; 2 | import { CommandTestFactory } from 'nest-commander-testing'; 3 | import { suite } from 'uvu'; 4 | import { equal } from 'uvu/assert'; 5 | import { LogService } from '../../common/log.service'; 6 | import { RequestProviderModule } from '../src/request-provider.module'; 7 | 8 | export const RequestProviderSuite = suite('Default Request Provider'); 9 | 10 | RequestProviderSuite('Default Request Provider', async () => { 11 | const logSpy = stubMethod(console, 'log'); 12 | const commandInstance = await CommandTestFactory.createTestingCommand({ 13 | imports: [RequestProviderModule], 14 | }) 15 | .overrideProvider(LogService) 16 | .useValue({ log: logSpy.handler }) 17 | .compile(); 18 | await CommandTestFactory.run(commandInstance, []); 19 | equal(logSpy.firstCall?.args[0], { custom: 'value' }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/question/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": { 4 | "type": "string", 5 | "minLength": 1, 6 | "x-prompt": "What is the questions set name?" 7 | }, 8 | "path": { 9 | "type": "string", 10 | "format": "path", 11 | "description": "The path to create the service." 12 | }, 13 | "sourceRoot": { 14 | "type": "string", 15 | "description": "Nest service source root directory." 16 | }, 17 | "flat": { 18 | "type": "boolean", 19 | "default": false, 20 | "description": "Flag to indicate if a directory is created.", 21 | "x-prompt": "Create a directory?" 22 | }, 23 | "spec": { 24 | "type": "boolean", 25 | "default": true, 26 | "description": "Specifies if a spec file is generated.", 27 | "x-prompt": "Generate spec file as well?" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /integration/help-tests/src/foo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LogService } from '../../common/log.service'; 3 | import { AfterAfterAllCommand } from './after-after-all.command'; 4 | import { AfterAllCommand } from './after-all.command'; 5 | import { AfterCommand } from './after.command'; 6 | import { BeforeAfterCommand } from './before-after.command'; 7 | import { BeforeAllCommand } from './before-all.command'; 8 | import { BeforeBeforeAllCommand } from './before-before-all.command'; 9 | import { BeforeCommand } from './before.command'; 10 | import { FooCommand } from './foo.command'; 11 | 12 | @Module({ 13 | providers: [ 14 | FooCommand, 15 | LogService, 16 | BeforeCommand, 17 | BeforeAllCommand, 18 | BeforeBeforeAllCommand, 19 | BeforeAfterCommand, 20 | BeforeAfterCommand, 21 | AfterCommand, 22 | AfterAfterAllCommand, 23 | AfterAllCommand, 24 | ], 25 | }) 26 | export class FooModule {} 27 | -------------------------------------------------------------------------------- /integration/with-questions/src/hello.command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CommandRunner, 4 | InquirerService, 5 | Option, 6 | } from 'nest-commander'; 7 | import { LogService } from '../../common/log.service'; 8 | import { HelloOptions } from './hello.interface'; 9 | 10 | @Command({ name: 'hello', options: { isDefault: true } }) 11 | export class HelloCommand extends CommandRunner { 12 | constructor( 13 | private readonly inquirer: InquirerService, 14 | private readonly logger: LogService, 15 | ) { 16 | super(); 17 | } 18 | 19 | async run(_inputs: string[], options?: HelloOptions): Promise { 20 | options = await this.inquirer.ask('hello', options); 21 | this.sayHello(options); 22 | } 23 | 24 | @Option({ 25 | flags: '-n --name [name]', 26 | }) 27 | parseName(val: string) { 28 | return val; 29 | } 30 | 31 | sayHello(options: HelloOptions): void { 32 | this.logger.log(`Hello ${options.name}`); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /integration/this-handler/src/this-handler.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Option } from 'nest-commander'; 2 | import { LogService } from '../../common/log.service'; 3 | 4 | @Command({ 5 | name: 'this-handler', 6 | description: 'Just adding a description for coverage', 7 | options: { isDefault: true }, 8 | }) 9 | export class ThisHandlerCommand extends CommandRunner { 10 | constructor(private readonly log: LogService) { 11 | super(); 12 | } 13 | 14 | async run(params: string[], options: Record) { 15 | this.logHandler(options); 16 | } 17 | 18 | logHandler(...args: any[]): void { 19 | this.log.log(...args); 20 | } 21 | 22 | @Option({ 23 | flags: '-b,--basic [basic]', 24 | description: 'A value to pass for the this-handler method', 25 | defaultValue: 'oh HAI', 26 | }) 27 | parseBasicOption(val: string): string { 28 | return this.handleParsingBasic(val); 29 | } 30 | 31 | handleParsingBasic(val: string): string { 32 | return val; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander-testing", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "type": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "dependsOn": [ 9 | { 10 | "target": "build", 11 | "projects": "dependencies" 12 | } 13 | ], 14 | "options": { 15 | "deleteOutputPath": true, 16 | "main": "packages/nest-commander-testing/src/index.ts", 17 | "outputPath": "dist/nest-commander-testing", 18 | "packageJson": "packages/nest-commander-testing/package.json", 19 | "tsConfig": "packages/nest-commander-testing/tsconfig.build.json", 20 | "assets": ["packages/nest-commander-testing/*.md"] 21 | } 22 | }, 23 | "publish": { 24 | "executor": "nx:run-commands", 25 | "options": { 26 | "cwd": "dist/nest-commander-testing", 27 | "command": "pnpm publish" 28 | } 29 | } 30 | }, 31 | "implicitDependencies": [] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2019-2022 Jay McDoniel, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander-schematics", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "type": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nx/js:tsc", 8 | "options": { 9 | "deleteOutputPath": true, 10 | "main": "packages/nest-commander-schematics/src/collection.json", 11 | "outputPath": "dist/nest-commander-schematics", 12 | "packageJson": "packages/nest-commander-schematics/package.json", 13 | "tsConfig": "packages/nest-commander-schematics/tsconfig.build.json", 14 | "assets": [ 15 | "packages/nest-commander-schematics/*.md", 16 | "packages/nest-commander-schematics/src/**/*.json", 17 | "packages/nest-commander-schematics/src/**/*.template" 18 | ] 19 | } 20 | }, 21 | "publish": { 22 | "executor": "nx:run-commands", 23 | "options": { 24 | "cwd": "dist/nest-commander-schematics", 25 | "command": "pnpm publish" 26 | } 27 | } 28 | }, 29 | "implicitDependencies": [] 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended', 7 | ], 8 | plugins: ['@typescript-eslint'], 9 | parserOptions: { 10 | source: 'module', 11 | ecmaVersion: 2018, 12 | }, 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | 'no-control-regex': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/interface-name-prefix': 'off', 23 | '@typescript-eslint/explicit-module-boundary-types': 'off', 24 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 25 | 'sort-imports': ['error', { ignoreDeclarationSort: true, ignoreCase: true }], 26 | 'prettier/prettier': 'warn', 27 | }, 28 | ignorePatterns: ['*.d.ts', 'dist/*', '**/node_modules/*', '*.js'], 29 | globals: { 30 | WeakSet: 'readonly', 31 | Promise: 'readonly', 32 | Reflect: 'readonly', 33 | Symbol: 'readonly', 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander-testing", 3 | "version": "3.5.1", 4 | "description": "A testing utility for nest-commander. It builds on top of ideas from @nestjs/testing and is not tied to any test framework directly.", 5 | "repository": { 6 | "type": "github", 7 | "url": "https://github.com/jmcdo29/nest-commander.git", 8 | "directory": "packages/nest-commander-testing" 9 | }, 10 | "keywords": [ 11 | "cli", 12 | "nestjs", 13 | "application", 14 | "command", 15 | "command-line", 16 | "nest", 17 | "decorator", 18 | "testing" 19 | ], 20 | "author": "Jay McDoniel ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jmcdo29/nest-commander/issues" 24 | }, 25 | "homepage": "https://nest-commander.jaymcdoniel.dev/docs/testing/installation", 26 | "peerDependencies": { 27 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 28 | "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 29 | "@nestjs/testing": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 30 | "nest-commander": "^2.5.0 || ^3.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/nest-commander/src/cli-utility.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class CliUtilityService { 5 | static trueValues: string[] = ['yes', '1', 'y', 'true', 't']; 6 | static falseValues: string[] = ['no', 'n', '0', 'false', 'f']; 7 | parseBoolean(val: string): boolean { 8 | val = val.toLowerCase(); 9 | const trueValue = CliUtilityService.trueValues.some((tVal) => tVal === val); 10 | const falseValue = CliUtilityService.falseValues.some( 11 | (fVal) => fVal === val, 12 | ); 13 | if (trueValue) { 14 | return true; 15 | } 16 | if (falseValue) { 17 | return false; 18 | } 19 | throw new Error( 20 | `${val} is not a proper value for a boolean input. Please use ${CliUtilityService.falseValues.join( 21 | ', ', 22 | )} for a "false" value or ${CliUtilityService.trueValues.join( 23 | ', ', 24 | )} for a "true" value`, 25 | ); 26 | } 27 | 28 | parseInt(val: string, radix = 10): number { 29 | return Number.parseInt(val, radix); 30 | } 31 | 32 | parseFloat(val: string): number { 33 | return Number.parseFloat(val); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "options": { 5 | "canTrackAnalytics": false, 6 | "showUsageWarnings": true 7 | } 8 | } 9 | }, 10 | "targetDependencies": { 11 | "publish": [ 12 | { 13 | "target": "build", 14 | "projects": "self" 15 | } 16 | ] 17 | }, 18 | "namedInputs": { 19 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 20 | "sharedGlobals": [ 21 | "{workspaceRoot}/workspace.json", 22 | "{workspaceRoot}/nx.json" 23 | ], 24 | "production": ["default"] 25 | }, 26 | "targetDefaults": { 27 | "build": { 28 | "inputs": ["production", "^production"], 29 | "cache": true 30 | }, 31 | "test": { 32 | "cache": true 33 | }, 34 | "lint": { 35 | "cache": true 36 | }, 37 | "package": { 38 | "cache": true 39 | }, 40 | "prepare": { 41 | "cache": true 42 | }, 43 | "e2e": { 44 | "cache": true 45 | } 46 | }, 47 | "nxCloudAccessToken": "MzRiYTQyYzMtNmQ2Zi00YjE1LWJhNTgtOTIxNDM0ZGU4YjMyfHJlYWQtd3JpdGU=", 48 | "parallel": 1, 49 | "useInferencePlugins": false, 50 | "defaultBase": "main" 51 | } 52 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander-schematics", 3 | "version": "3.2.0", 4 | "description": "A set of schematics for generating questions and commands with the NestJS CLI.", 5 | "scripts": { 6 | "prebuild": "rm -rf dist", 7 | "build": "tsc -p tsconfig.build.json", 8 | "postbuild": "./tools/schematics-postbuild", 9 | "test": "jest" 10 | }, 11 | "keywords": [ 12 | "schematics", 13 | "nest-commander", 14 | "commander", 15 | "nest", 16 | "nestjs", 17 | "cli", 18 | "inquirer" 19 | ], 20 | "author": "Jay McDoniel ", 21 | "license": "MIT", 22 | "files": [ 23 | "src" 24 | ], 25 | "schematics": "./src/collection.json", 26 | "homepage": "https://nest-commander.jaymcdoniel.dev/docs/schematics/installation/", 27 | "repository": { 28 | "type": "github", 29 | "url": "https://github.com/jmcdo29/nest-commander", 30 | "directory": "packages/nest-commander-schematics" 31 | }, 32 | "dependencies": { 33 | "@angular-devkit/core": "19.0.1", 34 | "@angular-devkit/schematics": "19.0.1", 35 | "rxjs": "7.8.2", 36 | "@nestjs/schematics": "11.0.0", 37 | "typescript": "5.7.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/SidebarToggle.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | import type { FunctionalComponent } from 'preact'; 3 | import { useState, useEffect } from 'preact/hooks'; 4 | 5 | const MenuToggle: FunctionalComponent = () => { 6 | const [sidebarShown, setSidebarShown] = useState(false); 7 | 8 | useEffect(() => { 9 | const body = document.querySelector('body')!; 10 | if (sidebarShown) { 11 | body.classList.add('mobile-sidebar-toggle'); 12 | } else { 13 | body.classList.remove('mobile-sidebar-toggle'); 14 | } 15 | }, [sidebarShown]); 16 | 17 | return ( 18 | 41 | ); 42 | }; 43 | 44 | export default MenuToggle; 45 | -------------------------------------------------------------------------------- /packages/nest-commander/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander", 3 | "version": "3.20.1", 4 | "description": "A module for making CLI applications with NestJS. Decorators for running commands and separating out config parsers included. This package works on top of commander.", 5 | "repository": { 6 | "type": "github", 7 | "url": "https://github.com/jmcdo29/nest-commander.git", 8 | "directory": "packages/nest-commander" 9 | }, 10 | "keywords": [ 11 | "cli", 12 | "nestjs", 13 | "application", 14 | "command", 15 | "command-line", 16 | "nest", 17 | "decorator" 18 | ], 19 | "author": "Jay McDoniel ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jmcdo29/nest-commander/issues" 23 | }, 24 | "homepage": "https://nest-commander.jaymcdoniel.dev", 25 | "dependencies": { 26 | "@fig/complete-commander": "^3.0.0", 27 | "@golevelup/nestjs-discovery": "5.0.0", 28 | "commander": "11.1.0", 29 | "cosmiconfig": "8.3.6", 30 | "inquirer": "8.2.7" 31 | }, 32 | "peerDependencies": { 33 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 34 | "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 35 | "@types/inquirer": "^8.1.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/LanguageSelect.css: -------------------------------------------------------------------------------- 1 | .language-select { 2 | flex-grow: 1; 3 | width: 48px; 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0.33em 0.5em; 7 | overflow: visible; 8 | font-weight: 500; 9 | font-size: 1rem; 10 | font-family: inherit; 11 | line-height: inherit; 12 | background-color: var(--theme-bg); 13 | border-color: var(--theme-text-lighter); 14 | color: var(--theme-text-light); 15 | border-style: solid; 16 | border-width: 1px; 17 | border-radius: 0.25rem; 18 | outline: 0; 19 | cursor: pointer; 20 | transition-timing-function: ease-out; 21 | transition-duration: 0.2s; 22 | transition-property: border-color, color; 23 | -webkit-font-smoothing: antialiased; 24 | padding-left: 30px; 25 | padding-right: 1rem; 26 | } 27 | .language-select-wrapper .language-select:hover, 28 | .language-select-wrapper .language-select:focus { 29 | color: var(--theme-text); 30 | border-color: var(--theme-text-light); 31 | } 32 | .language-select-wrapper { 33 | color: var(--theme-text-light); 34 | position: relative; 35 | } 36 | .language-select-wrapper > svg { 37 | position: absolute; 38 | top: 7px; 39 | left: 10px; 40 | pointer-events: none; 41 | } 42 | 43 | @media (min-width: 50em) { 44 | .language-select { 45 | width: 100%; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /integration/this-command/test/this-command.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { ThisCommandModule } from '../src/this-command.module'; 8 | 9 | export const ThisCommandHandlerSuite = suite<{ 10 | commandInstance: TestingModule; 11 | logMock: Stub; 12 | }>('This Command Handler Suite'); 13 | ThisCommandHandlerSuite.before(async (context) => { 14 | context.logMock = stubMethod(console, 'log'); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [ThisCommandModule], 17 | }) 18 | .overrideProvider(LogService) 19 | .useValue({ log: context.logMock.handler }) 20 | .compile(); 21 | }); 22 | ThisCommandHandlerSuite.after.each(({ logMock }) => { 23 | logMock.reset(); 24 | }); 25 | ThisCommandHandlerSuite( 26 | 'should call this-handler with arg hello', 27 | async ({ commandInstance, logMock }) => { 28 | await CommandTestFactory.run(commandInstance, ['this-command', 'hello']); 29 | equal(logMock.firstCall?.args[0], ['hello']); 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /integration/pizza/test/pizza.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { PizzaModule } from '../src/pizza.module'; 7 | 8 | export const PizzaSuite = suite<{ commandInstance: TestingModule }>( 9 | 'Pizza Command', 10 | ); 11 | PizzaSuite.before(async (context) => { 12 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 13 | imports: [PizzaModule], 14 | }).compile(); 15 | }); 16 | PizzaSuite( 17 | 'each option should be set-able from inquirer', 18 | async ({ commandInstance }) => { 19 | CommandTestFactory.setAnswers([ 20 | 'p', 21 | true, 22 | '9999999999', 23 | 'Large', 24 | 42, 25 | 'Coke', 26 | 'Nope, all good!', 27 | ]); 28 | const logMock = stubMethod(console, 'log'); 29 | await CommandTestFactory.run(commandInstance); 30 | equal(logMock.firstCall?.args[0], { 31 | toppings: 'PepperoniCheese', 32 | toBeDelivered: true, 33 | phone: '9999999999', 34 | size: 'large', 35 | quantity: 42, 36 | beverage: 'Coke', 37 | comments: 'Nope, all good!', 38 | }); 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /apps/docs/src/components/PageContent/PageContent.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Frontmatter } from '../../config'; 3 | import MoreMenu from '../RightSidebar/MoreMenu.astro'; 4 | import TableOfContents from '../RightSidebar/TableOfContents'; 5 | import type { MarkdownHeading } from 'astro'; 6 | 7 | type Props = { 8 | frontmatter: Frontmatter; 9 | headings: MarkdownHeading[]; 10 | githubEditUrl: string; 11 | }; 12 | 13 | const { frontmatter, headings, githubEditUrl } = Astro.props as Props; 14 | const title = frontmatter.title; 15 | --- 16 | 17 |
18 |
19 |

{title}

20 | 23 | 24 |
25 | 28 |
29 | 30 | 53 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/command/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "name": { 4 | "type": "string", 5 | "minLength": 1, 6 | "description": "Command name", 7 | "x-prompt": "What is the command name?" 8 | }, 9 | "path": { 10 | "type": "string", 11 | "format": "path", 12 | "description": "The path to create the service." 13 | }, 14 | "sourceRoot": { 15 | "type": "string", 16 | "description": "Nest service source root directory." 17 | }, 18 | "flat": { 19 | "type": "boolean", 20 | "default": false, 21 | "description": "Flag to indicate if a directory is created.", 22 | "x-prompt": "Create a directory?" 23 | }, 24 | "spec": { 25 | "type": "boolean", 26 | "default": true, 27 | "description": "Specifies if a spec file is generated.", 28 | "x-prompt": "Generate spec file as well?" 29 | }, 30 | "default": { 31 | "type": "boolean", 32 | "default": false, 33 | "description": "Specifies if the command is the default command for the CLI.", 34 | "x-prompt": "Is this the default command?" 35 | }, 36 | "question": { 37 | "type": "string", 38 | "x-prompt": "What is the name of the related question set? (Leave blank for none)" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/features/utility.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: UtilityService 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | As parsing booleans and numbers is a common occurrence, and the values only come in naturally as 7 | strings, nest-commander exposes a `CliUtilityService` to make parsing even easier and to add some 8 | extra utility. There are three methods, `parseBoolean`, `parseInt`, and `parseFloat`. `parseInt` and 9 | `parseFloat` are simple wrappers around the corresponding `Number.parse*()` method, but the 10 | `parseBoolean` method has a few more tricks to it. 11 | 12 | ## parseBoolean 13 | 14 | Sometimes with CLIs we want to have the simplest input as possible, and while `true` and `false` are 15 | clear, sometimes they're more than we really want to have to type out. Because of this, the 16 | `CliUtilityService` has a list of true and false values that can be accepted for a boolean input, 17 | even if it's not a boolean primitive. All inputs passed to `parseBoolean` are passed through 18 | `toLowerCase()` before any comparison is made. 19 | 20 | ### true Values 21 | 22 | Any string that matches one of the following values is considered a `true` input: 23 | 24 | - yes 25 | - y 26 | - true 27 | - t 28 | - 1 29 | 30 | ### false Values 31 | 32 | Any string that matches one of the following values is considered a `false` input: 33 | 34 | - no 35 | - n 36 | - false 37 | - f 38 | - 0 39 | -------------------------------------------------------------------------------- /integration/option-choices/src/option-choices.command.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Command, 3 | CommandRunner, 4 | Option, 5 | OptionChoiceFor, 6 | } from 'nest-commander'; 7 | 8 | import { LogService } from '../../common/log.service'; 9 | import { ChoicesProvider } from './choices-provider.service'; 10 | 11 | @Command({ name: 'options-test', options: { isDefault: true } }) 12 | export class OptionsTestCommand extends CommandRunner { 13 | constructor( 14 | private readonly logger: LogService, 15 | private readonly choiceProvider: ChoicesProvider, 16 | ) { 17 | super(); 18 | } 19 | async run(_args: never[], options: { choices: string }) { 20 | this.logger.log(options); 21 | } 22 | 23 | @Option({ 24 | name: 'choices', 25 | choices: true, 26 | description: 'The choices you can make for this. "yes" or "no"', 27 | flags: '-c, --choice [choices]', 28 | }) 29 | parseChoices(chosen: 'yes' | 'no') { 30 | return chosen; 31 | } 32 | 33 | @OptionChoiceFor({ name: 'choices' }) 34 | chosenForChoices() { 35 | return this.choiceProvider.getChoicesForChoicesOption(); 36 | } 37 | 38 | @Option({ 39 | choices: ['yes', 'no'], 40 | description: 'This is here to verify the parser is called', 41 | flags: '-v, [verify]', 42 | }) 43 | parseVerifyChoices(verifyChoice: string) { 44 | this.logger.log('verify choice parser'); 45 | return verifyChoice; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /integration/multiple/test/multiple.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { spy, Stub } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { MultipleCommandModule } from '../src/root.module'; 8 | 9 | export const MultipleCommandSuite = suite<{ 10 | logSpy: Stub; 11 | commandInstance: TestingModule; 12 | }>('Multiple Commands'); 13 | MultipleCommandSuite.before(async (context) => { 14 | context.logSpy = spy(); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [MultipleCommandModule], 17 | }) 18 | .overrideProvider(LogService) 19 | .useValue({ log: context.logSpy.handler }) 20 | .compile(); 21 | }); 22 | MultipleCommandSuite.after.each(({ logSpy }) => { 23 | logSpy.reset(); 24 | }); 25 | for (const { command, expected } of [ 26 | { 27 | command: 'foo', 28 | expected: 'foo', 29 | }, 30 | { 31 | command: 'bar', 32 | expected: 'bar', 33 | }, 34 | ] as const) { 35 | MultipleCommandSuite( 36 | `call ${command} expect ${expected}`, 37 | async ({ logSpy, commandInstance }) => { 38 | await CommandTestFactory.run( 39 | commandInstance, 40 | command ? [command] : undefined, 41 | ); 42 | equal(logSpy.firstCall?.args[0], expected); 43 | }, 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/nest-commander/src/command-factory.interface.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService, LogLevel } from '@nestjs/common'; 2 | import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface'; 3 | import { Help, OutputConfiguration } from 'commander'; 4 | import type { CompletionFactoryOptions } from './completion.factory.interface'; 5 | 6 | export type ErrorHandler = (err: Error) => void; 7 | export type ServiceErrorHandler = (err: Error) => PromiseLike | void; 8 | export type NestLogger = LoggerService | LogLevel[] | false; 9 | 10 | export interface DefinedCommandFactoryRunOptions 11 | extends CommandFactoryRunOptions { 12 | cliName: string; 13 | usePlugins: boolean; 14 | } 15 | 16 | export interface CommandFactoryRunOptions 17 | extends NestApplicationContextOptions { 18 | logger?: NestLogger; 19 | errorHandler?: ErrorHandler; 20 | usePlugins?: boolean; 21 | cliName?: string; 22 | serviceErrorHandler?: ServiceErrorHandler; 23 | enablePositionalOptions?: boolean; 24 | enablePassThroughOptions?: boolean; 25 | outputConfiguration?: OutputConfiguration; 26 | helpConfiguration?: Partial; 27 | version?: string; 28 | 29 | /** 30 | * Apply Bash, ZSH and Fig completion to your CLI 31 | * @default false 32 | */ 33 | completion?: false | CompletionFactoryOptions; 34 | } 35 | 36 | export interface CommanderOptionsType 37 | extends Omit { 38 | pluginsAvailable?: boolean; 39 | } 40 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/AstroLogo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | size: number; 4 | }; 5 | const { size } = Astro.props as Props; 6 | --- 7 | 8 | 41 | -------------------------------------------------------------------------------- /integration/with-questions/test/hello.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { HelloCommandModule } from '../src/root.module'; 8 | 9 | export const SetQuestionSuite = suite<{ 10 | commandInstance: TestingModule; 11 | logMock: Stub; 12 | }>('Set Question Suite'); 13 | SetQuestionSuite.before.each(async (context) => { 14 | context.logMock = stubMethod(console, 'log'); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [HelloCommandModule], 17 | }) 18 | .overrideProvider(LogService) 19 | .useValue({ log: context.logMock.handler }) 20 | .compile(); 21 | }); 22 | SetQuestionSuite.after.each(({ logMock }) => { 23 | logMock.reset(); 24 | }); 25 | for (const { name, expected } of [ 26 | { 27 | name: 'Jay', 28 | expected: 'Hello Jay', 29 | }, 30 | { 31 | name: 'Test', 32 | expected: 'Hello Test', 33 | }, 34 | ]) { 35 | SetQuestionSuite( 36 | `the arg ${name} should be passed to the command, populated from inquirer`, 37 | async ({ commandInstance, logMock }) => { 38 | logMock.passThrough(); 39 | CommandTestFactory.setAnswers(name); 40 | await CommandTestFactory.run(commandInstance); 41 | equal(logMock.firstCall?.args[0], expected); 42 | }, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /integration/this-handler/test/this-handler.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { ThisHandlerModule } from '../src/this-handler.module'; 8 | 9 | export const ThisOptionHandlerSuite = suite<{ 10 | commandInstance: TestingModule; 11 | logMock: Stub; 12 | }>('This Option Handler Suite'); 13 | ThisOptionHandlerSuite.before(async (context) => { 14 | context.logMock = stubMethod(console, 'log'); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [ThisHandlerModule], 17 | }) 18 | .overrideProvider(LogService) 19 | .useValue({ log: context.logMock.handler }) 20 | .compile(); 21 | }); 22 | ThisOptionHandlerSuite.after.each(({ logMock }) => { 23 | logMock.reset(); 24 | }); 25 | for (const { flagVal, expected } of [ 26 | { 27 | flagVal: ['-b'], 28 | expected: 'oh HAI', 29 | }, 30 | { 31 | flagVal: ['-b', 'hello'], 32 | expected: 'hello', 33 | }, 34 | ]) { 35 | ThisOptionHandlerSuite( 36 | `this-handler with arg ${flagVal} and log ${expected}`, 37 | async ({ commandInstance, logMock }) => { 38 | await CommandTestFactory.run( 39 | commandInstance, 40 | ['this-handler'].concat(flagVal), 41 | ); 42 | equal(logMock.firstCall?.args[0], { basic: expected }); 43 | }, 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/docs/src/components/HeadCommon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/theme.css'; 3 | import '../styles/index.css'; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 27 | 28 | 29 | 38 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature Request" 2 | description: "I have a suggestion \U0001F63B!" 3 | labels: ["enhancement", "needs triage"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: "Is there an existing issue that is already proposing this?" 8 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 9 | options: 10 | - label: "I have searched the existing issues" 11 | required: true 12 | 13 | - type: textarea 14 | validations: 15 | required: true 16 | attributes: 17 | label: "Is your feature request related to a problem? Please describe it" 18 | description: "A clear and concise description of what the problem is" 19 | placeholder: | 20 | I have an issue when ... 21 | 22 | - type: textarea 23 | validations: 24 | required: true 25 | attributes: 26 | label: "Describe the solution you'd like" 27 | description: "A clear and concise description of what you want to happen. Add any considered drawbacks" 28 | 29 | - type: textarea 30 | attributes: 31 | label: "Teachability, documentation, adoption, migration strategy" 32 | description: "If you can, explain how users will be able to use this and possibly write out a version the docs. Maybe a screenshot or design?" 33 | 34 | - type: textarea 35 | validations: 36 | required: true 37 | attributes: 38 | label: "What is the motivation / use case for changing the behavior?" 39 | description: "Describe the motivation or the concrete use case" 40 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/LanguageSelect.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import type { FunctionComponent } from 'react'; 3 | import './LanguageSelect.css'; 4 | import { KNOWN_LANGUAGES, langPathRegex } from '../../languages'; 5 | 6 | const LanguageSelect: FunctionComponent<{ lang: string }> = ({ lang }) => { 7 | return ( 8 |
9 | 27 | 45 |
46 | ); 47 | }; 48 | 49 | export default LanguageSelect; 50 | -------------------------------------------------------------------------------- /integration/root-command/test/root-command.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal, ok } from 'uvu/assert'; 6 | import { RootCommandModule } from '../src/root-command.module'; 7 | 8 | export const RootCommandSuite = suite<{ 9 | commandInstance: TestingModule; 10 | logMock: Stub; 11 | }>('RootCommand Test Suite'); 12 | 13 | RootCommandSuite.before(async (context) => { 14 | context.logMock = stubMethod(process.stdout, 'write'); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [RootCommandModule], 17 | }).compile(); 18 | }); 19 | 20 | RootCommandSuite.after.each(({ logMock }) => { 21 | logMock.reset(); 22 | }); 23 | RootCommandSuite.after(({ logMock }) => { 24 | logMock.restore(); 25 | }); 26 | 27 | RootCommandSuite( 28 | "it should return root command's help with -h", 29 | async ({ commandInstance, logMock }) => { 30 | const exitStub = stubMethod(process, 'exit'); 31 | const errMock = stubMethod(process.stderr, 'write'); 32 | try { 33 | await CommandTestFactory.run(commandInstance, ['-h']); 34 | } catch { 35 | // no op 36 | } 37 | ok( 38 | logMock.firstCall?.args[0].includes('-y, --yellow '), 39 | `Output help should include options from the @RootCommand() 40 | Instead, got ${logMock.firstCall?.args} 41 | `, 42 | ); 43 | equal( 44 | exitStub.firstCall?.args[0], 45 | 0, 46 | 'process.exit should be called with 0', 47 | ); 48 | exitStub.restore(); 49 | errMock.restore(); 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /integration/option-choices/test/option-choices.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { spy, Stub } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { OptionChoicesModule } from '../src/option-choices.module'; 8 | 9 | export const OptionChoiceSuite = suite<{ 10 | commandInstance: TestingModule; 11 | logMock: Stub; 12 | }>('OptionChoice Suite'); 13 | OptionChoiceSuite.before(async (context) => { 14 | context.logMock = spy(); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 16 | imports: [OptionChoicesModule], 17 | }) 18 | .overrideProvider(LogService) 19 | .useValue({ log: context.logMock.handler }) 20 | .compile(); 21 | }); 22 | OptionChoiceSuite.before.each(({ logMock }) => { 23 | logMock.reset(); 24 | }); 25 | OptionChoiceSuite( 26 | 'Send in option "yes"', 27 | async ({ commandInstance, logMock }) => { 28 | await CommandTestFactory.run(commandInstance, ['-c', 'yes']); 29 | equal(logMock.firstCall?.args[0], { choices: 'yes' }); 30 | }, 31 | ); 32 | OptionChoiceSuite( 33 | 'Send in option "no"', 34 | async ({ commandInstance, logMock }) => { 35 | await CommandTestFactory.run(commandInstance, ['-c', 'no']); 36 | equal(logMock.firstCall?.args[0], { choices: 'no' }); 37 | }, 38 | ); 39 | OptionChoiceSuite( 40 | 'Send in "yes" for verify', 41 | async ({ commandInstance, logMock }) => { 42 | await CommandTestFactory.run(commandInstance, ['-v', 'yes']); 43 | equal(logMock.firstCall?.args[0], 'verify choice parser'); 44 | }, 45 | ); 46 | 47 | OptionChoiceSuite.run(); 48 | -------------------------------------------------------------------------------- /integration/default-sub-commands/test/default-sub-commands.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { NestedModule } from '../src/nested.module'; 8 | 9 | export const DefaultSubCommandSuite = suite<{ 10 | logMock: Stub; 11 | exitMock: Stub; 12 | commandInstance: TestingModule; 13 | }>('Default Sub Command Suite'); 14 | DefaultSubCommandSuite.before(async (context) => { 15 | context.exitMock = stubMethod(process, 'exit'); 16 | context.logMock = stubMethod(console, 'log'); 17 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 18 | imports: [NestedModule], 19 | }) 20 | .overrideProvider(LogService) 21 | .useValue({ 22 | log: context.logMock.handler, 23 | }) 24 | .compile(); 25 | }); 26 | DefaultSubCommandSuite.after.each(({ logMock, exitMock }) => { 27 | logMock.reset(); 28 | exitMock.reset(); 29 | }); 30 | DefaultSubCommandSuite.after(({ exitMock }) => { 31 | exitMock.restore(); 32 | }); 33 | DefaultSubCommandSuite( 34 | 'top should call top mid1 bottom', 35 | async ({ commandInstance, logMock }) => { 36 | await CommandTestFactory.run(commandInstance, ['top']); 37 | equal(logMock.firstCall?.args[0], `top mid-1 bottom command`); 38 | }, 39 | ); 40 | DefaultSubCommandSuite( 41 | 'top mid-2 should call top mid2 command', 42 | async ({ commandInstance, logMock }) => { 43 | await CommandTestFactory.run(commandInstance, ['top', 'mid-2']); 44 | equal(logMock.firstCall?.args[0], `top mid-2 command`); 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /apps/docs/src/components/RightSidebar/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact'; 2 | import { useState, useEffect, useRef } from 'preact/hooks'; 3 | import type { MarkdownHeading } from 'astro'; 4 | 5 | type ItemOffsets = { 6 | id: string; 7 | topOffset: number; 8 | }; 9 | 10 | const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({ 11 | headings = [], 12 | }) => { 13 | const itemOffsets = useRef([]); 14 | // FIXME: Not sure what this state is doing. It was never set to anything truthy. 15 | const [activeId] = useState(''); 16 | useEffect(() => { 17 | const getItemOffsets = () => { 18 | const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)'); 19 | itemOffsets.current = Array.from(titles).map((title) => ({ 20 | id: title.id, 21 | topOffset: title.getBoundingClientRect().top + window.scrollY, 22 | })); 23 | }; 24 | 25 | getItemOffsets(); 26 | window.addEventListener('resize', getItemOffsets); 27 | 28 | return () => { 29 | window.removeEventListener('resize', getItemOffsets); 30 | }; 31 | }, []); 32 | 33 | return ( 34 | <> 35 |

On this page

36 |
    37 |
  • 38 | Overview 39 |
  • 40 | {headings 41 | .filter(({ depth }) => depth > 1 && depth < 4) 42 | .map((heading) => ( 43 |
  • 48 | {heading.text} 49 |
  • 50 | ))} 51 |
52 | 53 | ); 54 | }; 55 | 56 | export default TableOfContents; 57 | -------------------------------------------------------------------------------- /packages/nest-commander/src/command-runner.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Logger, Module, Type } from '@nestjs/common'; 2 | import { DiscoveryModule } from '@golevelup/nestjs-discovery'; 3 | import { Command } from 'commander'; 4 | import * as inquirer from 'inquirer'; 5 | import { CommanderOptionsType } from './command-factory.interface'; 6 | import { CommandRunnerService } from './command-runner.service'; 7 | import { Commander, CommanderOptions, Inquirer } from './constants'; 8 | import { InquirerService } from './inquirer.service'; 9 | import { CliUtilityService } from './cli-utility.service'; 10 | 11 | @Module({}) 12 | export class CommandRunnerModule { 13 | static inquirerOptions: { 14 | input: NodeJS.ReadStream; 15 | output: NodeJS.WriteStream; 16 | } = { 17 | input: process.stdin, 18 | output: process.stdout, 19 | }; 20 | static forModule( 21 | module?: Type | DynamicModule, 22 | options?: CommanderOptionsType, 23 | ): DynamicModule { 24 | return { 25 | global: true, 26 | module: CommandRunnerModule, 27 | imports: module ? [module, DiscoveryModule] : [DiscoveryModule], 28 | providers: [ 29 | Logger, 30 | CommandRunnerService, 31 | InquirerService, 32 | { 33 | provide: Commander, 34 | useClass: Command, 35 | }, 36 | { 37 | provide: Inquirer, 38 | useValue: inquirer, 39 | }, 40 | { 41 | provide: CommanderOptions, 42 | useValue: options ?? {}, 43 | }, 44 | { 45 | provide: 'InquirerOptions', 46 | useValue: this.inquirerOptions, 47 | }, 48 | CliUtilityService, 49 | ], 50 | exports: [InquirerService, CliUtilityService, Commander], 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | NX_RUN_GROUP: ${{ github.run_id }} 10 | NX_CLOUD_DISTRIBUTED_EXECUTION: true 11 | NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_TOKEN }} 12 | 13 | jobs: 14 | release: 15 | name: Release 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout Repo 19 | uses: actions/checkout@master 20 | with: 21 | fetch-depth: 0 22 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 23 | uses: nrwl/nx-set-shas@v4 24 | 25 | - name: Setup 26 | uses: ./.github/actions/setup 27 | 28 | - name: Build Projects 29 | run: pnpm build 30 | 31 | - name: Modify Workspace File 32 | # modify the current pnpm-workspace to point at `dist` instead of `packages` 33 | run: sed -e "s|'packages\/|'dist/|" pnpm-workspace.yaml > pnpm-new.yaml && mv pnpm-new.yaml pnpm-workspace.yaml 34 | 35 | - name: Create Release Pull Request or Publish to npm 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 40 | publish: pnpm release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | 45 | - name: Stop Nx Cloud Agents 46 | run: pnpm nx-cloud stop-all-agents 47 | 48 | nx_agent: 49 | runs-on: ubuntu-latest 50 | name: Nx Agent 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Setup 54 | uses: ./.github/actions/setup 55 | 56 | - name: Install Dependencies 57 | run: pnpm i 58 | - run: pnpm nx-cloud start-agent 59 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nest-commander-schematics 2 | 3 | ## 3.2.0 4 | 5 | ### Minor Changes 6 | 7 | - ca7ee4b: Bump @golevelup/nestjs-discovery from v4 to v5 8 | 9 | ## 3.1.0 10 | 11 | ### Minor Changes 12 | 13 | - ce3d4f4: Support NestJS v11 14 | 15 | ## 3.0.2 16 | 17 | ### Patch Changes 18 | 19 | - 08f72e5: Update angular changeset 20 | 21 | ## 3.0.1 22 | 23 | ### Patch Changes 24 | 25 | - 15b0d87: Actually publish the schematic templates 26 | 27 | ## 3.0.0 28 | 29 | ### Major Changes 30 | 31 | - 1bfc69f: Schematics now create command that extends the CommandRunner abstract 32 | class 33 | 34 | - 799b143: Update @nestjs/schematics to version 9 35 | 36 | There should not be "breaking" functionality, but there was a major version 37 | change of a dependent pacakge with no backwards support guaranteed. 38 | 39 | ### Patch Changes 40 | 41 | - 67662f6: fix typo 42 | 43 | ## 2.1.0 44 | 45 | ### Minor Changes 46 | 47 | - 9ef701c: Add prompt to every schematic option 48 | 49 | ## 2.0.1 50 | 51 | ### Patch Changes 52 | 53 | - b3c16cd: Fix the directory that the pnpm publish command looks at 54 | 55 | ## 2.0.0 56 | 57 | ### Major Changes 58 | 59 | - ee001cc: Upgrade all Nest dependencies to version 8 60 | 61 | WHAT: Upgrade `@nestjs/` dependencies to v8 and RxJS to v7 WHY: To support the 62 | latest version of Nest HOW: upgrading to Nest v8 should be all that's 63 | necessary (along with rxjs to v7) 64 | 65 | ## 1.0.0 66 | 67 | ### Major Changes 68 | 69 | - 28cc116: Releases two schematics for the angular and nest CLI's to make use of 70 | `command` and `question`. To use one, you can pass 71 | `--collection nest-commander-schematic` to Nest's CLI and then the schematic 72 | name followed by the name of the feature. e.g. 73 | `nest g --collection nest-commander-schematics command foo`. 74 | -------------------------------------------------------------------------------- /integration/basic/test/basic.command.factory.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { CommandTestFactory } from 'nest-commander-testing'; 3 | import { spy, Stub } from 'hanbi'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { RootModule } from '../src/root.module'; 8 | 9 | export const BasicFactorySuite = suite<{ 10 | commandInstance: TestingModule; 11 | logMock: Stub; 12 | args: string[]; 13 | }>('Basic Command With Factory'); 14 | 15 | BasicFactorySuite.before(async (context) => { 16 | context.logMock = spy(); 17 | context.args = ['basic', 'test']; 18 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 19 | imports: [RootModule], 20 | }) 21 | .overrideProvider(LogService) 22 | .useValue({ log: context.logMock.handler }) 23 | .compile(); 24 | }); 25 | 26 | BasicFactorySuite.after.each(({ logMock }) => { 27 | logMock.reset(); 28 | }); 29 | 30 | for (const { flagAndVal, expected } of [ 31 | { 32 | flagAndVal: ['--string=hello'], 33 | expected: { string: 'hello' }, 34 | }, 35 | { 36 | flagAndVal: ['-s', 'goodbye'], 37 | expected: { string: 'goodbye' }, 38 | }, 39 | { 40 | flagAndVal: ['--number=10'], 41 | expected: { number: 10 }, 42 | }, 43 | { 44 | flagAndVal: ['-n', '5'], 45 | expected: { number: 5 }, 46 | }, 47 | { 48 | flagAndVal: ['--boolean=true'], 49 | expected: { boolean: true }, 50 | }, 51 | { 52 | flagAndVal: ['-b', 'false'], 53 | expected: { boolean: false }, 54 | }, 55 | ]) { 56 | BasicFactorySuite( 57 | `${flagAndVal} \tlogs ${JSON.stringify(expected)}`, 58 | async ({ commandInstance, logMock, args }) => { 59 | await CommandTestFactory.run(commandInstance, [...args, ...flagAndVal]); 60 | equal(logMock.firstCall?.args[0], { param: ['test'], ...expected }); 61 | }, 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Deployment 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | workflow_dispatch: 8 | 9 | # Allow this job to clone the repo and create a page deployment 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | jobs: 16 | docs-changed: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | files: ${{ contains(steps.files.outputs.added_modified, 'apps/docs/') }} 20 | steps: 21 | - name: Get All Added and Modified Field 22 | id: files 23 | if: ${{ github.event_name != 'workflow_dispatch' }} 24 | uses: jitterbit/get-changed-files@v1 25 | - name: Echo contains 26 | if: ${{ github.event_name != 'workflow_dispatch' }} 27 | run: echo ${{ contains(steps.files.outputs.added_modified, 'apps/docs/') }} 28 | 29 | build-docs: 30 | runs-on: ubuntu-latest 31 | needs: 32 | - docs-changed 33 | if: ${{ needs.docs-changed.outputs.files == 'true' || github.event_name == 'workflow_dispatch' }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: '20.x' 39 | - name: Install pnpm 40 | run: npm i -g pnpm 41 | - name: Dependencies Install 42 | run: pnpm i 43 | - name: Build Docs 44 | run: pnpm nx build docs 45 | - name: Upload build 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | name: github-pages 49 | path: dist/apps/docs/ 50 | if-no-files-found: error 51 | 52 | docs-deploy: 53 | runs-on: ubuntu-latest 54 | needs: 55 | - build-docs 56 | permissions: 57 | pages: write 58 | id-token: write 59 | environment: 60 | name: github-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | steps: 63 | - name: GitHub Pages Deploy Step 64 | uses: actions/deploy-pages@v4 65 | id: deployment 66 | -------------------------------------------------------------------------------- /integration/version-option/test/version.option.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommandTestFactory } from 'nest-commander-testing'; 2 | import { stubMethod } from 'hanbi'; 3 | import { suite } from 'uvu'; 4 | import { equal } from 'uvu/assert'; 5 | import { RootModule } from '../src/root.module'; 6 | 7 | export const VersionOptionSuite = suite('Version option'); 8 | 9 | VersionOptionSuite('Running version, and version enabled', async () => { 10 | const version = '1.0.0'; 11 | const commandInstance = await CommandTestFactory.createTestingCommand( 12 | { 13 | imports: [RootModule], 14 | }, 15 | { 16 | version, 17 | }, 18 | ).compile(); 19 | let stdoutSpy = stubMethod(process.stdout, 'write'); 20 | const exitSpy = stubMethod(process, 'exit'); 21 | 22 | await CommandTestFactory.run(commandInstance, ['--version']); 23 | let stdOutResult = stdoutSpy.firstCall?.args[0] as string; 24 | equal(stdOutResult.includes(version), true); 25 | 26 | stdoutSpy.restore(); 27 | stdoutSpy = stubMethod(process.stdout, 'write'); 28 | 29 | await CommandTestFactory.run(commandInstance, ['-V']); 30 | stdOutResult = stdoutSpy.firstCall?.args[0] as string; 31 | equal(stdOutResult.includes(version), true); 32 | 33 | stdoutSpy.restore(); 34 | exitSpy.restore(); 35 | }); 36 | 37 | VersionOptionSuite( 38 | 'Running version, but version option is disabled', 39 | async () => { 40 | const commandInstance = await CommandTestFactory.createTestingCommand( 41 | { 42 | imports: [RootModule], 43 | }, 44 | { 45 | // no version option 46 | }, 47 | ).compile(); 48 | 49 | const stderrSpy = stubMethod(process.stderr, 'write'); 50 | const exitSpy = stubMethod(process, 'exit'); 51 | 52 | await CommandTestFactory.run(commandInstance, ['--version']); 53 | 54 | // error: unknown option '--version' 55 | equal(stderrSpy.firstCall?.args[0], `error: unknown option '--version'\n`); 56 | 57 | exitSpy.restore(); 58 | stderrSpy.restore(); 59 | }, 60 | ); 61 | -------------------------------------------------------------------------------- /integration/output-config/test/output.config.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { CommandTestFactory } from 'nest-commander-testing'; 3 | import { spy, Stub, stubMethod } from 'hanbi'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { RootModule } from '../src/root.module'; 7 | 8 | export const OutputConfigSuite = suite<{ 9 | commandInstance: TestingModule; 10 | errorLogMock: Stub; 11 | }>('OutputConfig Test suite'); 12 | 13 | OutputConfigSuite.before(async (context) => { 14 | context.errorLogMock = spy(); 15 | context.commandInstance = await CommandTestFactory.createTestingCommand( 16 | { 17 | imports: [RootModule], 18 | }, 19 | { 20 | outputConfiguration: { 21 | writeErr: (msg) => context.errorLogMock.handler(msg), 22 | }, 23 | serviceErrorHandler: (err) => { 24 | throw err; 25 | }, 26 | }, 27 | ).compile(); 28 | }); 29 | 30 | OutputConfigSuite.after.each(({ errorLogMock }) => { 31 | errorLogMock.reset(); 32 | }); 33 | 34 | OutputConfigSuite( 35 | 'outputConfig should be use in the root command', 36 | async ({ commandInstance, errorLogMock }) => { 37 | const exitSpy = stubMethod(process, 'exit'); 38 | // unknown command, should trigger an error 39 | await CommandTestFactory.run(commandInstance, ['unknown']); 40 | equal( 41 | errorLogMock.firstCall?.args[0], 42 | `error: unknown command 'unknown'\n`, 43 | ); 44 | exitSpy.restore(); 45 | }, 46 | ); 47 | 48 | OutputConfigSuite( 49 | 'outputConfig should have been passed down to subcommands', 50 | async ({ commandInstance, errorLogMock }) => { 51 | const exitSpy = stubMethod(process, 'exit'); 52 | // no args given, should trigger an error 53 | await CommandTestFactory.run(commandInstance, ['basic']); 54 | equal( 55 | errorLogMock.firstCall?.args[0], 56 | `error: required option '-n, --number ' not specified\n`, 57 | ); 58 | exitSpy.restore(); 59 | }, 60 | ); 61 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/Search.css: -------------------------------------------------------------------------------- 1 | /** Style Algolia */ 2 | :root { 3 | --docsearch-primary-color: var(--theme-accent); 4 | --docsearch-logo-color: var(--theme-text); 5 | } 6 | .search-input { 7 | flex-grow: 1; 8 | box-sizing: border-box; 9 | width: 100%; 10 | margin: 0; 11 | padding: 0.33em 0.5em; 12 | overflow: visible; 13 | font-weight: 500; 14 | font-size: 1rem; 15 | font-family: inherit; 16 | line-height: inherit; 17 | background-color: var(--theme-divider); 18 | border-color: var(--theme-divider); 19 | color: var(--theme-text-light); 20 | border-style: solid; 21 | border-width: 1px; 22 | border-radius: 0.25rem; 23 | outline: 0; 24 | cursor: pointer; 25 | transition-timing-function: ease-out; 26 | transition-duration: 0.2s; 27 | transition-property: border-color, color; 28 | -webkit-font-smoothing: antialiased; 29 | } 30 | .search-input:hover, 31 | .search-input:focus { 32 | color: var(--theme-text); 33 | border-color: var(--theme-text-light); 34 | } 35 | .search-input:hover::placeholder, 36 | .search-input:focus::placeholder { 37 | color: var(--theme-text-light); 38 | } 39 | .search-input::placeholder { 40 | color: var(--theme-text-light); 41 | } 42 | .search-hint { 43 | position: absolute; 44 | top: 7px; 45 | right: 19px; 46 | padding: 3px 5px; 47 | display: none; 48 | display: none; 49 | align-items: center; 50 | justify-content: center; 51 | letter-spacing: 0.125em; 52 | font-size: 13px; 53 | font-family: var(--font-mono); 54 | pointer-events: none; 55 | border-color: var(--theme-text-lighter); 56 | color: var(--theme-text-light); 57 | border-style: solid; 58 | border-width: 1px; 59 | border-radius: 0.25rem; 60 | line-height: 14px; 61 | } 62 | 63 | @media (min-width: 50em) { 64 | .search-hint { 65 | display: flex; 66 | } 67 | } 68 | 69 | /* ------------------------------------------------------------ *\ 70 | DocSearch (Algolia) 71 | \* ------------------------------------------------------------ */ 72 | 73 | .DocSearch-Modal .DocSearch-Hit a { 74 | box-shadow: none; 75 | border: 1px solid var(--theme-accent); 76 | } 77 | -------------------------------------------------------------------------------- /apps/docs/src/components/HeadSEO.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { SITE, OPEN_GRAPH, Frontmatter } from '../config'; 3 | 4 | export interface Props { 5 | frontmatter: Frontmatter; 6 | canonicalUrl: URL; 7 | } 8 | 9 | const { frontmatter, canonicalUrl } = Astro.props as Props; 10 | const formattedContentTitle = `${frontmatter.title} 🚀 ${SITE.title}`; 11 | const imageSrc = frontmatter.image?.src ?? OPEN_GRAPH.image.src; 12 | const canonicalImageSrc = new URL(imageSrc, Astro.site); 13 | const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt; 14 | --- 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | -------------------------------------------------------------------------------- /integration/basic/src/basic.command.ts: -------------------------------------------------------------------------------- 1 | import { Command, CommandRunner, Option } from 'nest-commander'; 2 | import { LogService } from './../../common/log.service'; 3 | 4 | interface BasicCommandOptions { 5 | string?: string; 6 | boolean?: boolean; 7 | number?: number; 8 | } 9 | 10 | @Command({ name: 'basic', description: 'A parameter parse' }) 11 | export class BasicCommand extends CommandRunner { 12 | constructor(private readonly logService: LogService) { 13 | super(); 14 | } 15 | 16 | async run( 17 | passedParam: string[], 18 | options?: BasicCommandOptions, 19 | ): Promise { 20 | if (options?.boolean !== undefined && options?.boolean !== null) { 21 | this.runWithBoolean(passedParam, options.boolean); 22 | } else if (options?.number) { 23 | this.runWithNumber(passedParam, options.number); 24 | } else if (options?.string) { 25 | this.runWithString(passedParam, options.string); 26 | } else { 27 | this.runWithNone(passedParam); 28 | } 29 | } 30 | 31 | @Option({ 32 | flags: '-n, --number [number]', 33 | description: 'A basic number parser', 34 | }) 35 | parseNumber(val: string): number { 36 | return Number(val); 37 | } 38 | 39 | @Option({ 40 | flags: '-s, --string [string]', 41 | description: 'A string return', 42 | }) 43 | parseString(val: string): string { 44 | return val; 45 | } 46 | 47 | @Option({ 48 | flags: '-b, --boolean [boolean]', 49 | description: 'A boolean parser', 50 | }) 51 | parseBoolean(val: string): boolean { 52 | return JSON.parse(val); 53 | } 54 | 55 | runWithString(param: string[], option: string): void { 56 | this.logService.log({ param, string: option }); 57 | } 58 | 59 | runWithNumber(param: string[], option: number): void { 60 | this.logService.log({ param, number: option }); 61 | } 62 | 63 | runWithBoolean(param: string[], option: boolean): void { 64 | this.logService.log({ param, boolean: option }); 65 | } 66 | 67 | runWithNone(param: string[]): void { 68 | this.logService.log({ param }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /integration/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { BasicFactorySuite } from './basic/test/basic.command.factory.spec'; 2 | import { 3 | BooleanCommandSuite, 4 | NumberCommandSuite, 5 | StringCommandSuite, 6 | UnknownCommandSuite, 7 | } from './basic/test/basic.command.spec'; 8 | import { DefaultSubCommandSuite } from './default-sub-commands/test/default-sub-commands.spec'; 9 | import { DotCommandSuite } from './dot-command/test/dot.command.spec'; 10 | import { HelpSuite } from './help-tests/test/help.spec'; 11 | import { MultipleCommandSuite } from './multiple/test/multiple.command.spec'; 12 | import { OptionChoiceSuite } from './option-choices/test/option-choices.spec'; 13 | import { PizzaSuite } from './pizza/test/pizza.command.spec'; 14 | import { PluginSuite } from './plugins/test/plugin.command.spec'; 15 | import { SubCommandSuite } from './sub-commands/test/sub-commands.spec'; 16 | import { ThisCommandHandlerSuite } from './this-command/test/this-command.command.spec'; 17 | import { ThisOptionHandlerSuite } from './this-handler/test/this-handler.command.spec'; 18 | import { SetQuestionSuite } from './with-questions/test/hello.command.spec'; 19 | import { RegisterWithSubCommandsSuite } from './register-provider/test/register-with-subcommands.spec'; 20 | import { RequestProviderSuite } from './request-provider-override/test/index.spec'; 21 | import { RootCommandSuite } from './root-command/test/root-command.spec'; 22 | import { OutputConfigSuite } from './output-config/test/output.config.spec'; 23 | import { VersionOptionSuite } from './version-option/test/version.option.spec'; 24 | 25 | BasicFactorySuite.run(); 26 | StringCommandSuite.run(); 27 | NumberCommandSuite.run(); 28 | BooleanCommandSuite.run(); 29 | UnknownCommandSuite.run(); 30 | HelpSuite.run(); 31 | MultipleCommandSuite.run(); 32 | PizzaSuite.run(); 33 | PluginSuite.run(); 34 | SubCommandSuite.run(); 35 | ThisCommandHandlerSuite.run(); 36 | ThisOptionHandlerSuite.run(); 37 | SetQuestionSuite.run(); 38 | OptionChoiceSuite.run(); 39 | DotCommandSuite.run(); 40 | RegisterWithSubCommandsSuite.run(); 41 | DefaultSubCommandSuite.run(); 42 | RequestProviderSuite.run(); 43 | RootCommandSuite.run(); 44 | OutputConfigSuite.run(); 45 | VersionOptionSuite.run(); 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/regression.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4A5 Regression" 2 | description: "Report an unexpected behavior while upgrading nest-commander" 3 | labels: ["needs triage"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: "Is there an existing issue that is already proposing this?" 8 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the feature you are requesting" 9 | options: 10 | - label: "I have searched the existing issues" 11 | required: true 12 | 13 | - type: input 14 | attributes: 15 | label: "Potential Commit/PR that introduced the regression" 16 | description: "If you have time to investigate, what PR/date/version introduced this issue" 17 | placeholder: "PR #123 or commit 5b3c4a4" 18 | 19 | - type: input 20 | attributes: 21 | label: "Versions" 22 | description: "From which version of `nest-commander` to which version you are upgrading" 23 | placeholder: "2.3.0 -> 2.3.1" 24 | 25 | - type: textarea 26 | validations: 27 | required: true 28 | attributes: 29 | label: "Describe the regression" 30 | description: "A clear and concise description of what the regression is" 31 | 32 | - type: textarea 33 | attributes: 34 | label: "Minimum reproduction code" 35 | description: | 36 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 37 | **Tip:** If you leave a minimum repository, we will help you faster! 38 | value: | 39 | ```ts 40 | 41 | ``` 42 | 43 | - type: textarea 44 | validations: 45 | required: true 46 | attributes: 47 | label: "Expected behavior" 48 | description: "A clear and concise description of what you expected to happend (or code)" 49 | 50 | - type: textarea 51 | attributes: 52 | label: "Other" 53 | description: | 54 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 55 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 56 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/README.md: -------------------------------------------------------------------------------- 1 | # NestJS Commander Testing 2 | 3 | So you;'ve built a CLI application, but you want to test it, and you want to be able to do your 4 | usual NestJS DI mocking. Well, here's your solution :fireworks: 5 | 6 | ## Installation 7 | 8 | Before you get started, you'll need to install a few packages. First and foremost, this one: 9 | `nest-commander-testing` (name pending). You'll also need to install `@nestjs/testing` as this 10 | package makes use of them under the hood, but doesn't want to tie you down to a specific version, 11 | yay peerDependencies! 12 | 13 | ```sh 14 | npm i nest-commander-testing @nestjs/testing 15 | # OR 16 | yarn add nest-commander-testing @nestjs/testing 17 | # OR 18 | pnpm i nest-commander-testing @nestjs/testing 19 | ``` 20 | 21 | ## Testing With Mocks 22 | 23 | So what's the use of writing a super awesome command line script if you can't test it super easily, 24 | right? Fortunately, `nest-commander` has some utilities you can make use of that fits in perfectly 25 | with the NestJS ecosystem, it'll feel right at home to any Nestlings out there. Instead of using the 26 | `CommandFactory` for building the command in test mode, you can use `CommandTestFactory` and pass in 27 | your metadata, very similarly to how `Test.createTestingModule` from `@nestjs/testing` works. In 28 | fact, it uses this package under the hood. You're also still able to chain on the `overrideProvider` 29 | methods before calling `compile()` so you can swap out DI pieces right in the test. 30 | [A nice example of this can be seen in the basic.command.factory.spec.ts file](./../../integration/basic/test/basic.command.factory.spec.ts). 31 | 32 | ## Testing Inquirer Questions 33 | 34 | If you are making use of the `InquirerService`, you can use `CommandTestFactory.setAnswers` method 35 | and pass either a single answer or multiple answers depending on how many answers you need to mock. 36 | In doing so, a mock inquirer service will act similarly to inquirer without needing to modify 37 | `process.stdin` or make use of any user input, allowing smooth testing in your CI pipelines. For an 38 | example of this, please check the 39 | [pizza command integration test](../../integration/pizza/test/pizza.command.spec.ts). 40 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/features/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | As of version 2.3.0, you can build your CLI with the ability to read for extra plugins that are 7 | developed by other people. By using the `usePlugins` option with the `CommandFactory`, you'll be 8 | setting up you shiny new CLI to expect to find a config file with a `plugins` property that is an 9 | array of strings, either as the locations of packages in a local environment, or npm package names. 10 | 11 | ## The Config File 12 | 13 | The config file, by default, can be _one_ of the following: 14 | 15 | - `.nest-commanderrc` 16 | - `.nest-commanderrc.json` 17 | - `.nest-commanderrc.yaml` 18 | - `.nest-commanderrc.yml` 19 | - `nest-commander.json` 20 | - `nest-commander.yaml` 21 | - `nest-commander.yml` 22 | 23 | If you'd like to use a name other than `nest-commander`, you can pass the `cliName` option to the 24 | `CommandFactory` as well. 25 | 26 | Now the config file should be incredibly simple, just a JSON object with a `plugins` property that 27 | is an array of strings, e.g. 28 | 29 | ```json 30 | { 31 | "plugins": ["nest-commander-plugin", "./my/local/plugin"] 32 | } 33 | ``` 34 | 35 | ## The Plugins 36 | 37 | Each plugin registered needs to have a **default** export that is a Nest module that adds the new 38 | command as a `provider`. 39 | 40 | ```ts title=src/plugin.command.ts 41 | @Command({ name: 'plugin' }) 42 | export class PluginCommand extends CommandRunner { 43 | async run() { 44 | console.log('From the plugin!'); 45 | } 46 | } 47 | ``` 48 | 49 | ```ts title=src/plugin.module.ts 50 | @Module({ 51 | providers: [PluginCommand] 52 | }) 53 | export class PluginModule {} 54 | ``` 55 | 56 | ```ts title=src/index.ts 57 | import { PluginModule } from './plugin.module'; 58 | export default PluginModule; 59 | ``` 60 | 61 | :::info 62 | 63 | If the command you've built uses `usePlugins: true` and a config file is not found, commander will 64 | still be allowed to try and execute the command given. If an error ends up being thrown, such as a 65 | command not found error, then the user of the CLI will get a warning about a possible config file 66 | missing, along with commander's standard help message. 67 | 68 | ::: 69 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/schematics/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | :::info 7 | 8 | We'll show how to use the `nest-commander-schematics` with 9 | [`@nestjs/cli`](https://www.npmjs.com/package/@nestjs/cli), but it works with `@angular/cli` and 10 | `nx` as well, as all of them use Angular's schematics under the hood. 11 | 12 | ::: 13 | 14 | ## Generating Commands 15 | 16 | To generate a simple command you can use the `command` schematic: 17 | 18 | ``` 19 | nest g -c nest-commander-schematics command 20 | ``` 21 | 22 | from there the wizard will ask what the name of the command is and if you would like to add 23 | questions. You can choose _no_ or provide a question set name at this point. 24 | 25 | The available options for this command are the following: 26 | 27 | ``` 28 | --name= Command name. 29 | --path= The path to create the service. 30 | --sourceRoot= NestJS service source root directory. 31 | --flat Whether or not a directory is created. (default: false) 32 | --spec Whether or not a spec file is generated. (default: true) 33 | --default Whether or not the command is the default command for the CLI. (default: false) 34 | --question= The name of the related question set. 35 | ``` 36 | 37 | ### Generating Commands with Questions 38 | 39 | As mentioned above, you can use the `command` schematic and provide a question set name. You can 40 | also use `--question=` to provide a name for the question set without waiting to answer the 41 | prompt. 42 | 43 | ## Generating Questions 44 | 45 | You can also generate a question set using the `question` schematic: 46 | 47 | ``` 48 | nest g -c nest-commander-schematics question 49 | ``` 50 | 51 | from there you can provide a name for the question set as mentioned by the wizard. 52 | 53 | The available options for this command are the following: 54 | 55 | ``` 56 | --name= Questions set name. 57 | --path= The path to create the service. 58 | --sourceRoot= NestJS service source root directory. 59 | --flat Whether or not a directory is created. (default: false) 60 | --spec Whether or not a spec file is generated. (default: true) 61 | ``` 62 | -------------------------------------------------------------------------------- /apps/docs/src/components/RightSidebar/MoreMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ThemeToggleButton from './ThemeToggleButton'; 3 | import * as CONFIG from '../../config'; 4 | 5 | type Props = { 6 | editHref: string; 7 | }; 8 | 9 | const { editHref } = Astro.props as Props; 10 | const showMoreSection = CONFIG.COMMUNITY_INVITE_URL; 11 | --- 12 | 13 | {showMoreSection &&

More

} 14 | 71 |
72 | 73 |
74 | 75 | 83 | -------------------------------------------------------------------------------- /apps/docs/src/components/RightSidebar/ThemeToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact'; 2 | import { useState, useEffect } from 'preact/hooks'; 3 | import './ThemeToggleButton.css'; 4 | 5 | const themes = ['light', 'dark']; 6 | 7 | const icons = [ 8 | 15 | 20 | , 21 | 28 | 29 | , 30 | ]; 31 | 32 | const ThemeToggle: FunctionalComponent = () => { 33 | const [theme, setTheme] = useState(() => { 34 | if (import.meta.env.SSR) { 35 | return undefined; 36 | } 37 | if (typeof localStorage !== undefined && localStorage.getItem('theme')) { 38 | return localStorage.getItem('theme'); 39 | } 40 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 41 | return 'dark'; 42 | } 43 | return 'light'; 44 | }); 45 | 46 | useEffect(() => { 47 | const root = document.documentElement; 48 | if (theme === 'light') { 49 | root.classList.remove('theme-dark'); 50 | } else { 51 | root.classList.add('theme-dark'); 52 | } 53 | }, [theme]); 54 | 55 | return ( 56 |
57 | {themes.map((t, i) => { 58 | const icon = icons[i]; 59 | const checked = t === theme; 60 | return ( 61 | 76 | ); 77 | })} 78 |
79 | ); 80 | }; 81 | 82 | export default ThemeToggle; 83 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nest-commander-testing 2 | 3 | ## 3.5.1 4 | 5 | ### Patch Changes 6 | 7 | - 0e8288b: Fix relative directory to package in repository 8 | 9 | ## 3.5.0 10 | 11 | ### Minor Changes 12 | 13 | - ca7ee4b: Bump @golevelup/nestjs-discovery from v4 to v5 14 | 15 | ## 3.4.0 16 | 17 | ### Minor Changes 18 | 19 | - ce3d4f4: Support NestJS v11 20 | 21 | ## 3.3.0 22 | 23 | ### Minor Changes 24 | 25 | - 519018e: Add ability to set outputConfiguration. 26 | 27 | Now CommandFactory.run(), CommandFactory.runWithoutClosing() and 28 | CommandFactory.createWithoutRunning() accept the option `outputConfiguration`. 29 | 30 | ## 3.2.0 31 | 32 | ### Minor Changes 33 | 34 | - 1fa92a0: Support NestJS v10 35 | 36 | ## 3.1.0 37 | 38 | ### Minor Changes 39 | 40 | - c75ca13: Add runWithoutClosing for testing 41 | 42 | ## 3.0.1 43 | 44 | ### Patch Changes 45 | 46 | - 23b2f48: Add 3.0.0 to peer deps 47 | 48 | ## 3.0.0 49 | 50 | ### Major Changes 51 | 52 | - d6ebe0e: Migrate `CommandRunner` from interface to abstract class and add 53 | `.command` 54 | 55 | This change was made so that devs could access `this.command` inside the 56 | `CommandRunner` instance and have access to the base command object from 57 | commander. This allows for access to the `help` commands in a programatic 58 | fashion. 59 | 60 | To update to this version, any `implements CommandRunner` should be changed to 61 | `extends CommandRunner`. If there is a `constructor` to the `CommandRunner` 62 | then it should also use `super()`. 63 | 64 | ### Minor Changes 65 | 66 | - 3d2aa9e: Update NestJS package to version 9 67 | 68 | ## 2.0.1 69 | 70 | ### Patch Changes 71 | 72 | - 3831e52: Adds a new `@Help()` decorator for custom commander help output 73 | 74 | `nest-commander-testing` now also uses a `hex` instead of `utf-8` encoding 75 | when creating a random js file name during the `CommandTestFactory` command. 76 | This is to help create more predictable output names. 77 | 78 | ## 2.0.0 79 | 80 | ### Major Changes 81 | 82 | - ee001cc: Upgrade all Nest dependencies to version 8 83 | 84 | WHAT: Upgrade `@nestjs/` dependencies to v8 and RxJS to v7 WHY: To support the 85 | latest version of Nest HOW: upgrading to Nest v8 should be all that's 86 | necessary (along with rxjs to v7) 87 | 88 | ## 1.2.0 89 | 90 | ### Minor Changes 91 | 92 | - f3f687b: Allow for commands to be run indefinitely 93 | 94 | There is a new `runWithoutClosing` method in the `CommandFactory` class. This 95 | command allows for not having the created Nest Application get closed 96 | immediately, which should allow for the use of indefinitely runnable commands. 97 | -------------------------------------------------------------------------------- /integration/sub-commands/test/sub-commands.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { NestedModule } from '../src/nested.module'; 8 | 9 | export const SubCommandSuite = suite<{ 10 | logMock: Stub; 11 | exitMock: Stub; 12 | commandInstance: TestingModule; 13 | }>('Sub Command Suite'); 14 | SubCommandSuite.before(async (context) => { 15 | context.exitMock = stubMethod(process, 'exit'); 16 | context.logMock = stubMethod(console, 'log'); 17 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 18 | imports: [NestedModule], 19 | }) 20 | .overrideProvider(LogService) 21 | .useValue({ 22 | log: context.logMock.handler, 23 | }) 24 | .compile(); 25 | }); 26 | SubCommandSuite.after.each(({ logMock, exitMock }) => { 27 | logMock.reset(); 28 | exitMock.reset(); 29 | }); 30 | SubCommandSuite.after(({ exitMock }) => { 31 | exitMock.restore(); 32 | }); 33 | for (const command of [ 34 | ['top'], 35 | ['top', 'mid-1'], 36 | ['top', 'mid-1', 'bottom'], 37 | ['top', 'mid-2'], 38 | ]) { 39 | SubCommandSuite( 40 | `run the ${command} command`, 41 | async ({ commandInstance, logMock }) => { 42 | await CommandTestFactory.run(commandInstance, command); 43 | equal(logMock.firstCall?.args[0], `${command.join(' ')} command`); 44 | }, 45 | ); 46 | } 47 | SubCommandSuite( 48 | 'parameters should still be passable', 49 | async ({ commandInstance, logMock }) => { 50 | await CommandTestFactory.run(commandInstance, ['top', 'hello!']); 51 | equal(logMock.callCount, 2); 52 | equal(logMock.firstCall?.args[0], 'top command'); 53 | equal(logMock.getCall(1).args[0], ['hello!']); 54 | }, 55 | ); 56 | for (const command of ['mid-1', 'mid-2', 'bottom']) { 57 | SubCommandSuite( 58 | `write an error from ${command} command`, 59 | async ({ commandInstance, logMock, exitMock }) => { 60 | const errStub = stubMethod(process.stderr, 'write'); 61 | await CommandTestFactory.run(commandInstance, [command]); 62 | equal(logMock.callCount, 0); 63 | equal(exitMock.firstCall?.args[0], 1); 64 | errStub.restore(); 65 | }, 66 | ); 67 | } 68 | SubCommandSuite( 69 | 'SubCommand mid-2 should be callable with "m"', 70 | async ({ commandInstance, logMock }) => { 71 | await CommandTestFactory.run(commandInstance, ['top', 'm']); 72 | equal(logMock.firstCall?.args[0], 'top mid-2 command'); 73 | }, 74 | ); 75 | -------------------------------------------------------------------------------- /integration/register-provider/test/register-with-subcommands.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule } from '@nestjs/testing'; 2 | import { Stub, stubMethod } from 'hanbi'; 3 | import { CommandTestFactory } from 'nest-commander-testing'; 4 | import { suite } from 'uvu'; 5 | import { equal } from 'uvu/assert'; 6 | import { LogService } from '../../common/log.service'; 7 | import { NestedModule } from '../src/nested.module'; 8 | 9 | export const RegisterWithSubCommandsSuite = suite<{ 10 | logMock: Stub; 11 | exitMock: Stub; 12 | commandInstance: TestingModule; 13 | }>('Register With SubCommands Suite'); 14 | RegisterWithSubCommandsSuite.before(async (context) => { 15 | context.exitMock = stubMethod(process, 'exit'); 16 | context.logMock = stubMethod(console, 'log'); 17 | context.commandInstance = await CommandTestFactory.createTestingCommand({ 18 | imports: [NestedModule], 19 | }) 20 | .overrideProvider(LogService) 21 | .useValue({ 22 | log: context.logMock.handler, 23 | }) 24 | .compile(); 25 | }); 26 | RegisterWithSubCommandsSuite.after.each(({ logMock, exitMock }) => { 27 | logMock.reset(); 28 | exitMock.reset(); 29 | }); 30 | RegisterWithSubCommandsSuite.after(({ exitMock }) => { 31 | exitMock.restore(); 32 | }); 33 | for (const command of [ 34 | ['top'], 35 | ['top', 'mid-1'], 36 | ['top', 'mid-1', 'bottom'], 37 | ['top', 'mid-2'], 38 | ]) { 39 | RegisterWithSubCommandsSuite( 40 | `run the ${command} command`, 41 | async ({ commandInstance, logMock }) => { 42 | await CommandTestFactory.run(commandInstance, command); 43 | equal(logMock.firstCall?.args[0], `${command.join(' ')} command`); 44 | }, 45 | ); 46 | } 47 | RegisterWithSubCommandsSuite( 48 | 'parameters should still be passable', 49 | async ({ commandInstance, logMock }) => { 50 | await CommandTestFactory.run(commandInstance, ['top', 'hello!']); 51 | equal(logMock.callCount, 2); 52 | equal(logMock.firstCall?.args[0], 'top command'); 53 | equal(logMock.getCall(1).args[0], ['hello!']); 54 | }, 55 | ); 56 | for (const command of ['mid-1', 'mid-2', 'bottom']) { 57 | RegisterWithSubCommandsSuite( 58 | `write an error from ${command} command`, 59 | async ({ commandInstance, logMock, exitMock }) => { 60 | const errStub = stubMethod(process.stderr, 'write'); 61 | await CommandTestFactory.run(commandInstance, [command]); 62 | equal(logMock.callCount, 0); 63 | equal(exitMock.firstCall?.args[0], 1); 64 | errStub.restore(); 65 | }, 66 | ); 67 | } 68 | RegisterWithSubCommandsSuite( 69 | 'RegisterProvider mid-2 should be callable with "m"', 70 | async ({ commandInstance, logMock }) => { 71 | await CommandTestFactory.run(commandInstance, ['top', 'm']); 72 | equal(logMock.firstCall?.args[0], 'top mid-2 command'); 73 | }, 74 | ); 75 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/Search.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | import { useState, useCallback, useRef } from 'react'; 3 | import { ALGOLIA } from '../../config'; 4 | import '@docsearch/css'; 5 | import './Search.css'; 6 | 7 | import { createPortal } from 'react-dom'; 8 | import * as docSearchReact from '@docsearch/react'; 9 | 10 | /** FIXME: This is still kinda nasty, but DocSearch is not ESM ready. */ 11 | const DocSearchModal = 12 | docSearchReact.DocSearchModal || (docSearchReact as any).default.DocSearchModal; 13 | const useDocSearchKeyboardEvents = 14 | docSearchReact.useDocSearchKeyboardEvents || 15 | (docSearchReact as any).default.useDocSearchKeyboardEvents; 16 | 17 | export default function Search() { 18 | const [isOpen, setIsOpen] = useState(false); 19 | const searchButtonRef = useRef(null); 20 | const [initialQuery, setInitialQuery] = useState(''); 21 | 22 | const onOpen = useCallback(() => { 23 | setIsOpen(true); 24 | }, [setIsOpen]); 25 | 26 | const onClose = useCallback(() => { 27 | setIsOpen(false); 28 | }, [setIsOpen]); 29 | 30 | const onInput = useCallback( 31 | (e) => { 32 | setIsOpen(true); 33 | setInitialQuery(e.key); 34 | }, 35 | [setIsOpen, setInitialQuery] 36 | ); 37 | 38 | useDocSearchKeyboardEvents({ 39 | isOpen, 40 | onOpen, 41 | onClose, 42 | onInput, 43 | searchButtonRef, 44 | }); 45 | 46 | return ( 47 | <> 48 | 69 | 70 | {isOpen && 71 | createPortal( 72 | { 80 | return items.map((item) => { 81 | // We transform the absolute URL into a relative URL to 82 | // work better on localhost, preview URLS. 83 | const a = document.createElement('a'); 84 | a.href = item.url; 85 | const hash = a.hash === '#overview' ? '' : a.hash; 86 | return { 87 | ...item, 88 | url: `${a.pathname}${hash}`, 89 | }; 90 | }); 91 | }} 92 | />, 93 | document.body 94 | )} 95 | 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /packages/nest-commander-schematics/src/common/index.ts: -------------------------------------------------------------------------------- 1 | import { Path, strings } from '@angular-devkit/core'; 2 | import { 3 | apply, 4 | applyTemplates, 5 | branchAndMerge, 6 | chain, 7 | filter, 8 | mergeWith, 9 | move, 10 | noop, 11 | Rule, 12 | SchematicContext, 13 | SchematicsException, 14 | Source, 15 | Tree, 16 | url, 17 | } from '@angular-devkit/schematics'; 18 | import { 19 | DeclarationOptions, 20 | Location, 21 | mergeSourceRoot, 22 | ModuleDeclarator, 23 | ModuleFinder, 24 | NameParser, 25 | } from '@nestjs/schematics'; 26 | import { join } from 'path'; 27 | import { CommonOptions } from './common-options.interface'; 28 | 29 | export class CommonSchematicFactory { 30 | templatePath = './files'; 31 | type = 'service'; 32 | metadata = 'providers'; 33 | create(options: T): Rule { 34 | options = this.transform(options); 35 | return branchAndMerge( 36 | chain([ 37 | mergeSourceRoot(options), 38 | this.addDeclarationToModule(options), 39 | mergeWith(this.generate(options)), 40 | ]), 41 | ); 42 | } 43 | generate(options: T): Source { 44 | return (context: SchematicContext) => 45 | apply(url(join(this.templatePath as Path)), [ 46 | options.spec 47 | ? noop() 48 | : filter((path: string) => !path.endsWith('.spec.ts')), 49 | applyTemplates({ 50 | ...strings, 51 | ...options, 52 | lowercase: (str: string) => str.toLowerCase(), 53 | }), 54 | move(options.path ?? ''), 55 | ])(context); 56 | } 57 | addDeclarationToModule(options: T): Rule { 58 | return (tree: Tree) => { 59 | options.module = new ModuleFinder(tree).find({ 60 | name: options.name, 61 | path: options.path as Path, 62 | }); 63 | if (options.module === undefined || options.module === null) { 64 | return tree; 65 | } 66 | const rawContent = tree.read(options.module); 67 | const content = rawContent?.toString() ?? ''; 68 | const declarator: ModuleDeclarator = new ModuleDeclarator(); 69 | tree.overwrite( 70 | options.module, 71 | declarator.declare(content, options as DeclarationOptions), 72 | ); 73 | return tree; 74 | }; 75 | } 76 | 77 | transform(source: T): T { 78 | const target: T = Object.assign({}, source); 79 | target.metadata = this.metadata; 80 | target.type = this.type; 81 | 82 | if (target.name === null || target.name === undefined) { 83 | throw new SchematicsException('Option (name) is required.'); 84 | } 85 | const location: Location = new NameParser().parse(target); 86 | target.name = strings.dasherize(location.name); 87 | target.path = strings.dasherize(location.path); 88 | 89 | target.path = target.flat 90 | ? target.path 91 | : join(target.path as Path, target.name); 92 | return target; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/introduction/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: Why nest-commander? 4 | layout: ../../../layouts/MainLayout.astro 5 | --- 6 | 7 | ## Initial Motivation 8 | 9 | [NestJS](https://docs.nestjs.com) is a super powerful NodeJS framework that allows developers to 10 | have the same architecture across all their applications. But other than the mention of 11 | [Standalone Applications](https://docs.nestjs.com/standalone-applications) there's only a few 12 | packages that attempt to bring together Nest's opinionated architecture and popular CLI packages. 13 | `nest-commander` aims to bring the most unified experience when it comes to being inline with Nest's 14 | ideologies and opinions, by wrapping the popular [Commander](https://github.com/tj/commander.js) and 15 | [Inquirer](https://github.com/SBoudrias/Inquirer.js) packages, and providing its own decorators for 16 | integration with all the corresponding libraries. 17 | 18 | ## Plugins 19 | 20 | [Plugins](/en/features/plugins) raise nest-commander to the next level of CLI programming. With 21 | plugins, you, the CLI developer, are able to split out commands between global CLI and project 22 | specific CLI commands. Imagine, at some point, you'll notice that certain commands need to be 23 | separately evolved to run apace to a certain project. Or, you'll need different commands for 24 | different project types. So, instead of creating different versions of your one global CLI, with 25 | plugins, you could split out the local and global commands to their own packages. Plugins allow for 26 | version matching of a project's specialized CLI commands to different versions or types of projects. 27 | As you can imagine, this enables you to build very intricate CLIs. 28 | 29 | The plugins feature will more likely be needed later in your project's evolution. That is, once your 30 | CLI needs grow, this ability to "break out" commands is ready and waiting for you to go to the next 31 | level of CLI development. 32 | 33 | ## Code Reuse of Your Nest Code - in the CLI 34 | 35 | One of the biggest advantages to Nest's modularization techniques is the ability to separate 36 | standard or commonly used code to their own modules and build them as standalone libraries. With 37 | nest-commander, such libraries can also be used by your CLI commands too. Take, for instance, the 38 | scenario where you might need to build your own data seeding or data initialization scripts, where 39 | they only need to be ran once to start off a project. Running these scripts are perfect as CLI 40 | commands. And, instead of you creating new modules or having to copy code just for the CLI commands 41 | to gain access to your database layer to do the work, you simply use the same modules already built 42 | for your project, leveraging all of the great advantages Nest's DI system has to offer, at the same 43 | time. 44 | 45 | All these reasons are why nest-commander is THE perfect companion to allow you to build flexible and 46 | smart CLI applications for your Nest-based projects. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug Report" 2 | description: "If something isn't working as expected \U0001F914" 3 | labels: ["needs triage", "bug"] 4 | body: 5 | - type: checkboxes 6 | attributes: 7 | label: "Is there an existing issue for this?" 8 | description: "Please search [here](./?q=is%3Aissue) to see if an issue already exists for the bug you encountered" 9 | options: 10 | - label: "I have searched the existing issues" 11 | required: true 12 | 13 | - type: textarea 14 | validations: 15 | required: true 16 | attributes: 17 | label: "Current behavior" 18 | description: "How the issue manifests?" 19 | 20 | - type: textarea 21 | validations: 22 | required: true 23 | attributes: 24 | label: "Minimum reproduction code" 25 | description: | 26 | Please share a git repo, a gist, or step-by-step instructions. [Wtf is a minimum reproduction?](https://jmcdo29.github.io/wtf-is-a-minimum-reproduction) 27 | **Tip**: If you leave a minimum repository, we can help you faster! 28 | placeholder: | 29 | ```ts 30 | 31 | ``` 32 | 33 | 1. `npm i` 34 | 2. `npm start:dev` 35 | 3. See error... 36 | 37 | - type: textarea 38 | validations: 39 | required: true 40 | attributes: 41 | label: "Expected behavior" 42 | description: "A clear and concise description of what you expected to happend (or code)" 43 | 44 | - type: markdown 45 | attributes: 46 | value: | 47 | --- 48 | 49 | - type: checkboxes 50 | validations: 51 | required: true 52 | attributes: 53 | label: "Package" 54 | description: | 55 | Which package (or packages) do you think your issue is related to? 56 | **Tip**: The first line of the stack trace can help you to figure out this 57 | options: 58 | - label: "nest-commander" 59 | - label: "nest-commander-schematics" 60 | - label: "nest-commander-testing" 61 | 62 | - type: input 63 | validations: 64 | required: true 65 | attributes: 66 | label: "Package version" 67 | description: "Which version of `nest-commander` are you using?" 68 | placeholder: "2.3.0" 69 | 70 | - type: markdown 71 | attributes: 72 | value: | 73 | --- 74 | 75 | - type: input 76 | attributes: 77 | label: "Node.js version" 78 | description: "Which version of Node.js are you using?" 79 | placeholder: "14.17.6" 80 | 81 | - type: checkboxes 82 | attributes: 83 | label: "In which operating systems have you tested?" 84 | options: 85 | - label: macOS 86 | - label: Windows 87 | - label: Linux 88 | 89 | - type: textarea 90 | attributes: 91 | label: "Other" 92 | description: | 93 | Anything else relevant? eg: Logs, OS version, IDE, package manager, etc. 94 | **Tip:** You can attach images, recordings or log files by clicking this area to highlight it and then dragging files in 95 | -------------------------------------------------------------------------------- /integration/pizza/src/pizza.question.ts: -------------------------------------------------------------------------------- 1 | import { Question, QuestionSet, ValidateFor, WhenFor } from 'nest-commander'; 2 | 3 | @QuestionSet({ name: 'pizza' }) 4 | export class PizzaQuestion { 5 | @Question({ 6 | type: 'expand', 7 | name: 'toppings', 8 | message: 'What about the toppings?', 9 | choices: [ 10 | { 11 | key: 'p', 12 | name: 'Pepperoni and cheese', 13 | value: 'PepperoniCheese', 14 | }, 15 | { 16 | key: 'a', 17 | name: 'All dressed', 18 | value: 'alldressed', 19 | }, 20 | { 21 | key: 'w', 22 | name: 'Hawaiian', 23 | value: 'hawaiian', 24 | }, 25 | ], 26 | }) 27 | parseToppings(val: string) { 28 | return val; 29 | } 30 | 31 | @Question({ 32 | type: 'confirm', 33 | name: 'toBeDelivered', 34 | message: 'Is this for delivery?', 35 | default: false, 36 | }) 37 | parseToBeConfirmed(val: boolean) { 38 | return val; 39 | } 40 | 41 | @Question({ 42 | type: 'input', 43 | name: 'phone', 44 | message: "What's your phone number?", 45 | }) 46 | parsePhone(val: string) { 47 | return val; 48 | } 49 | 50 | @ValidateFor({ name: 'phone' }) 51 | validatePhone(value: string) { 52 | const pass = value.match( 53 | /^([01]{1})?[-.\s]?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?){1}(?:\d+)?)?$/i, 54 | ); 55 | if (pass) { 56 | return true; 57 | } 58 | 59 | return 'Please enter a valid phone number'; 60 | } 61 | 62 | @Question({ 63 | type: 'list', 64 | name: 'size', 65 | message: 'What size do you need?', 66 | choices: ['Large', 'Medium', 'Small'], 67 | }) 68 | parseSize(val: string) { 69 | return val.toLowerCase(); 70 | } 71 | 72 | @Question({ 73 | type: 'input', 74 | name: 'quantity', 75 | message: 'How many do you need?', 76 | }) 77 | parseQuantity(val: string) { 78 | return Number(val); 79 | } 80 | 81 | @ValidateFor({ name: 'quantity' }) 82 | validateQuantity(val: string) { 83 | const valid = !isNaN(parseFloat(val)); 84 | return valid || 'Please enter a number'; 85 | } 86 | 87 | @Question({ 88 | type: 'rawlist', 89 | name: 'beverage', 90 | message: 'You also get a free 2L beverage', 91 | choices: ['Pepsi', '7up', 'Coke'], 92 | }) 93 | parseBeverage(val: string) { 94 | return val; 95 | } 96 | 97 | @Question({ 98 | type: 'input', 99 | name: 'comments', 100 | message: 'Any comments on your purchase experience?', 101 | default: 'Nope, all good!', 102 | }) 103 | parseComments(val: string) { 104 | return val; 105 | } 106 | 107 | @Question({ 108 | type: 'list', 109 | name: 'prize', 110 | message: 'For leaving a comment, you get a freebie', 111 | choices: ['cake', 'fries'], 112 | }) 113 | parsePrize(val: string) { 114 | return val; 115 | } 116 | 117 | @WhenFor({ name: 'prize' }) 118 | whenPrize(answers: { comments: string }): boolean { 119 | return answers.comments !== 'Nope, all good!'; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/testing/factory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CommandTestFactory 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | To get started with the `CommandTestFactory` you need to make use of the `createTestingCommand`, 7 | similar to `TestingModule`'s `createTestingModule`. This command can take in general module 8 | metadata, including providers, but generally it's pretty easy to just take in the related module and 9 | use `overrideProvider` for mocking whatever providers are necessary to mock. 10 | 11 | ## Mocking Commands 12 | 13 | Normally when running a CLI you'd do something like 14 | ` [options]`, right, something like 15 | `crun run 'echo Hello World!'`, but that's harder to do in a testing environment. With our 16 | `CommandTestFactory` instead, we can do something like the following: 17 | 18 | ```typescript title="test/task.command.spec.ts" 19 | describe('Task Command', () => { 20 | let commandInstance: TestingModule; 21 | 22 | beforeAll(async () => { 23 | commandInstance = await CommandTestFactory.createTestingCommand({ 24 | imports: [AppModule] 25 | }).compile(); 26 | }); 27 | 28 | it('should call the "run" method', async () => { 29 | const spawnSpy = jest.spyOn(childProcess, 'spawn'); 30 | await CommandTestFactory.run(commandInstance, ['run', 'echo Hello World!']); 31 | expect(spawnSpy).toBeCalledWith(['echo Hello World!', { shell: os.userInfo().shell }]); 32 | }); 33 | }); 34 | ``` 35 | 36 | :::tip 37 | 38 | `TestingModule` is imported from `@nestjs/testing` package. 39 | 40 | ::: 41 | 42 | Aside from the Jest spies that we're using, you'll notice that we use the `CommandTestFactory` to 43 | set up a `TestingModule` and use it to run a test command. We pass the `run` command here to match 44 | our `@Command()` we already created, but because `run` is the default command, it can be omitted. 45 | Then we pass in our arguments as the next array value, and any flags would be array values after it. 46 | All of this gets passed on to the commander instance and is processed as usual. 47 | 48 | ## Mocking User Input 49 | 50 | Now this is great and all, but we also need to be able to mock user inputs, as we allow the 51 | `InquirerService` to take in responses to questions. For this, we can use 52 | `CommandTestFactory.setAnswers()`. We can pass an array of answers to the `setAnswers` method to 53 | mock the input gained from the user. 54 | 55 | ```typescript title="test/task.command.spec.ts" 56 | describe('Task Command', () => { 57 | let commandInstance: TestingModule; 58 | 59 | beforeAll(async () => { 60 | commandInstance = await CommandTestFactory.createTestingCommand({ 61 | imports: [AppModule] 62 | }).compile(); 63 | }); 64 | 65 | it('should call the "run" method', async () => { 66 | CommandTestFactory.setAnswers(['echo Hello World!']); 67 | const spawnSpy = jest.spyOn(childProcess, 'spawn'); 68 | await CommandTestFactory.run(commandInstance, ['run']); 69 | expect(spawnSpy).toBeCalledWith(['echo Hello World!', { shell: os.userInfo().shell }]); 70 | }); 71 | }); 72 | ``` 73 | 74 | :::tip 75 | 76 | The answers passed in will be what are passed back from the `InquirerService`'s `ask` method, so 77 | make sure to have already transformed the input as the `InquirerService` would. 78 | 79 | ::: 80 | -------------------------------------------------------------------------------- /apps/docs/src/config.ts: -------------------------------------------------------------------------------- 1 | export const SITE = { 2 | title: 'Nest-Commander', 3 | description: 'Using NestJS as a CLI builder', 4 | defaultLanguage: 'en_US', 5 | }; 6 | 7 | export const OPEN_GRAPH = { 8 | image: { 9 | src: 'https://repository-images.githubusercontent.com/328917508/8aed1c58-81dc-4561-9c52-14924e2dfb08', 10 | alt: 'nest-commander logo, the NestJS cat on top of a right facing arrow with an underscore under the cat', 11 | }, 12 | twitter: 'jmcdo29', 13 | }; 14 | 15 | // This is the type of the frontmatter you put in the docs markdown files. 16 | export type Frontmatter = { 17 | title: string; 18 | description: string; 19 | layout: string; 20 | image?: { src: string; alt: string }; 21 | dir?: 'ltr' | 'rtl'; 22 | ogLocale?: string; 23 | lang?: string; 24 | }; 25 | 26 | export const KNOWN_LANGUAGES = { 27 | English: 'en', 28 | } as const; 29 | export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES); 30 | 31 | export const GITHUB_EDIT_URL = `https://github.com/jmcdo29/nest-commander/tree/main/apps/docs`; 32 | 33 | export const COMMUNITY_INVITE_URL = `https://discord.gg/6byqVsXzaF`; 34 | 35 | export const GITHUB_DISCUSSIONS_URL = `https://github.com/jmcdo29/nest-commander/discussions`; 36 | 37 | // See "Algolia" section of the README for more information. 38 | export const ALGOLIA = { 39 | indexName: 'nest-commander', 40 | appId: '9O0K4CXI15', 41 | apiKey: '9689faf6550ca3133e69be1d9861ea92', 42 | }; 43 | 44 | export type Sidebar = Record< 45 | (typeof KNOWN_LANGUAGE_CODES)[number], 46 | Record 47 | >; 48 | export const SIDEBAR: Sidebar = { 49 | en: { 50 | Introduction: [ 51 | { text: 'Why nest-commander?', link: 'en/introduction/intro' }, 52 | { 53 | text: 'Installation', 54 | link: 'en/introduction/installation', 55 | }, 56 | ], 57 | Features: [ 58 | { 59 | text: 'Commander', 60 | link: 'en/features/commander', 61 | }, 62 | { 63 | text: 'Inquirer', 64 | link: 'en/features/inquirer', 65 | }, 66 | { 67 | text: 'Command Factory', 68 | link: 'en/features/factory', 69 | }, 70 | { 71 | text: 'Completion', 72 | link: 'en/features/completion', 73 | }, 74 | { 75 | text: 'Plugins', 76 | link: 'en/features/plugins', 77 | }, 78 | { 79 | text: 'Utility Service', 80 | link: 'en/features/utility', 81 | }, 82 | ], 83 | Testing: [ 84 | { 85 | text: 'Installation', 86 | link: 'en/testing/installation', 87 | }, 88 | { 89 | text: 'TestFactory', 90 | link: 'en/testing/factory', 91 | }, 92 | ], 93 | Schematics: [ 94 | { 95 | text: 'Installation', 96 | link: 'en/schematics/installation', 97 | }, 98 | { 99 | text: 'Usage', 100 | link: 'en/schematics/usage', 101 | }, 102 | ], 103 | 'Execution and Publishing': [ 104 | { 105 | text: 'Overview', 106 | link: 'en/execution', 107 | }, 108 | ], 109 | API: [ 110 | { 111 | text: 'Overview', 112 | link: 'en/api', 113 | }, 114 | ], 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /apps/docs/src/components/LeftSidebar/LeftSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getLanguageFromURL } from '../../languages'; 3 | import { SIDEBAR } from '../../config'; 4 | 5 | type Props = { 6 | currentPage: string; 7 | }; 8 | 9 | const { currentPage } = Astro.props as Props; 10 | const currentPageMatch = currentPage.endsWith('/') 11 | ? currentPage.slice(1, -1) 12 | : currentPage.slice(1); 13 | const langCode = getLanguageFromURL(currentPage); 14 | const sidebar = SIDEBAR[langCode]; 15 | --- 16 | 17 | 40 | 41 | 55 | 56 | 126 | 127 | 132 | -------------------------------------------------------------------------------- /apps/docs/src/components/Header/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getLanguageFromURL, KNOWN_LANGUAGE_CODES } from '../../languages'; 3 | import * as CONFIG from '../../config'; 4 | import AstroLogo from './AstroLogo.astro'; 5 | import SkipToContent from './SkipToContent.astro'; 6 | import SidebarToggle from './SidebarToggle'; 7 | import LanguageSelect from './LanguageSelect'; 8 | import Search from './Search'; 9 | 10 | type Props = { 11 | currentPage: string; 12 | }; 13 | 14 | const { currentPage } = Astro.props as Props; 15 | const lang = getLanguageFromURL(currentPage); 16 | --- 17 | 18 |
19 | 20 | 36 |
37 | 38 | 144 | 145 | 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-commander-monorepo", 3 | "version": "0.0.0", 4 | "description": "A module for making CLI applications with NestJS. Decorators for running commands and separating out config parsers included. This package works on top of commander.", 5 | "scripts": { 6 | "build": "nx affected:build --parallel", 7 | "lint": "eslint --ext .ts .", 8 | "format": "prettier \"{packages,integration}/**/{src,test}/*.ts\"", 9 | "format:check": "pnpm format -- --check", 10 | "format:write": "pnpm format -- --write", 11 | "postinstall": "husky install", 12 | "release": "changeset publish", 13 | "nx": "nx", 14 | "deploy": "nx deploy docs", 15 | "e2e": "nx e2e integration" 16 | }, 17 | "private": true, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/jmcdo29/nest-commander.git" 21 | }, 22 | "keywords": [ 23 | "cli", 24 | "nestjs", 25 | "application", 26 | "command", 27 | "command-line", 28 | "nest", 29 | "decorator" 30 | ], 31 | "author": "Jay McDoniel ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/jmcdo29/nest-commander/issues" 35 | }, 36 | "homepage": "https://jmcdo29.github.io/nest-commander", 37 | "devDependencies": { 38 | "@algolia/client-search": "^5.0.0", 39 | "@angular-devkit/core": "19.1.3", 40 | "@angular-devkit/schematics": "19.1.3", 41 | "@angular-devkit/schematics-cli": "19.1.3", 42 | "@astrojs/preact": "^4.0.0", 43 | "@astrojs/react": "^4.0.0", 44 | "@astrojs/sitemap": "^3.0.0", 45 | "@changesets/cli": "2.27.12", 46 | "@commitlint/cli": "19.6.1", 47 | "@commitlint/config-conventional": "19.6.0", 48 | "@docsearch/css": "^3.3.0", 49 | "@docsearch/react": "^3.3.0", 50 | "@golevelup/nestjs-discovery": "5.0.0", 51 | "@mdx-js/react": "3.1.0", 52 | "@nestjs/cli": "11.0.2", 53 | "@nestjs/common": "11.0.16", 54 | "@nestjs/core": "11.0.6", 55 | "@nestjs/schematics": "11.0.0", 56 | "@nestjs/testing": "11.0.6", 57 | "@nx/js": "20.2.1", 58 | "@nx/node": "20.2.1", 59 | "@nx/workspace": "20.2.1", 60 | "@swc/core": "1.10.11", 61 | "@swc/register": "0.1.10", 62 | "@types/inquirer": "8.2.12", 63 | "@types/node": "22.10.10", 64 | "@types/react": "^19.0.0", 65 | "@types/react-dom": "^19.0.0", 66 | "@typescript-eslint/eslint-plugin": "6.21.0", 67 | "@typescript-eslint/parser": "6.21.0", 68 | "astro": "^5.0.0", 69 | "c8": "10.1.3", 70 | "clsx": "2.1.1", 71 | "commander": "11.1.0", 72 | "conventional-changelog-cli": "5.0.0", 73 | "cosmiconfig": "8.3.6", 74 | "cz-conventional-changelog": "3.3.0", 75 | "eslint": "8.57.1", 76 | "eslint-config-prettier": "9.1.2", 77 | "eslint-plugin-prettier": "5.5.4", 78 | "hanbi": "1.0.3", 79 | "hastscript": "^9.0.0", 80 | "husky": "9.1.7", 81 | "inquirer": "8.2.7", 82 | "lint-staged": "15.2.10", 83 | "nx": "20.2.1", 84 | "nx-uvu": "1.3.1", 85 | "pinst": "3.0.0", 86 | "preact": "^10.13.2", 87 | "prettier": "3.6.2", 88 | "react": "19.0.0", 89 | "react-dom": "19.0.0", 90 | "reflect-metadata": "0.2.2", 91 | "remark-directive": "^3.0.0", 92 | "rxjs": "7.8.2", 93 | "typescript": "5.7.2", 94 | "unist-util-visit": "^5.0.0", 95 | "url-loader": "4.1.1", 96 | "uvu": "0.5.6" 97 | }, 98 | "resolutions": { 99 | "terser": "^5.0.0", 100 | "reflect-metadata": "0.2.2" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/nest-commander/src/command-runner.interface.ts: -------------------------------------------------------------------------------- 1 | import { DiscoveredMethodWithMeta } from '@golevelup/nestjs-discovery'; 2 | import { ClassProvider, Type } from '@nestjs/common'; 3 | import { Command, CommandOptions } from 'commander'; 4 | import type { 5 | CheckboxQuestion, 6 | ConfirmQuestion, 7 | EditorQuestion, 8 | ExpandQuestion, 9 | InputQuestion, 10 | ListQuestion, 11 | NumberQuestion, 12 | PasswordQuestion, 13 | RawListQuestion, 14 | } from 'inquirer'; 15 | import { CommandMeta, SubCommandMeta } from './constants'; 16 | 17 | export type InquirerKeysWithPossibleFunctionTypes = 18 | | 'transformer' 19 | | 'validate' 20 | | 'when' 21 | | 'choices' 22 | | 'message' 23 | | 'default'; 24 | 25 | type InquirerQuestionWithoutFilter = Omit; 26 | 27 | type CommandRunnerClass = ClassProvider & typeof CommandRunner; 28 | 29 | export abstract class CommandRunner { 30 | static registerWithSubCommands( 31 | meta: string = CommandMeta, 32 | ): CommandRunnerClass[] { 33 | // NOTE: "this' in the scope is inherited class 34 | const subcommands: CommandRunnerClass[] = 35 | Reflect.getMetadata(meta, this)?.subCommands || []; 36 | return subcommands.reduce( 37 | (current: CommandRunnerClass[], subcommandClass: CommandRunnerClass) => { 38 | const results = subcommandClass.registerWithSubCommands(SubCommandMeta); 39 | return [...current, ...results]; 40 | }, 41 | [this] as CommandRunnerClass[], 42 | ); 43 | } 44 | protected command!: Command; 45 | public setCommand(command: Command): this { 46 | this.command = command; 47 | return this; 48 | } 49 | abstract run( 50 | passedParams: string[], 51 | options?: Record, 52 | ): Promise; 53 | } 54 | 55 | export interface CommandMetadata { 56 | name: string; 57 | arguments?: string; 58 | description?: string; 59 | argsDescription?: Record; 60 | options?: CommandOptions; 61 | subCommands?: Array>; 62 | aliases?: string[]; 63 | allowUnknownOptions?: boolean; 64 | allowExcessArgs?: boolean; 65 | } 66 | 67 | export type RootCommandMetadata = Omit & { 68 | name?: string; 69 | }; 70 | 71 | export interface OptionMetadata { 72 | flags: string; 73 | description?: string; 74 | defaultValue?: string | boolean | number; 75 | required?: boolean; 76 | name?: string; 77 | choices?: string[] | true; 78 | env?: string; 79 | } 80 | 81 | export interface OptionChoiceForMetadata { 82 | name: string; 83 | } 84 | 85 | export interface RunnerMeta { 86 | instance: CommandRunner; 87 | command: RootCommandMetadata; 88 | params: DiscoveredMethodWithMeta[]; 89 | help?: DiscoveredMethodWithMeta[]; 90 | } 91 | 92 | export interface QuestionNameMetadata { 93 | name: string; 94 | } 95 | 96 | export type QuestionMetadata = 97 | | InquirerQuestionWithoutFilter 98 | | InquirerQuestionWithoutFilter 99 | | InquirerQuestionWithoutFilter 100 | | InquirerQuestionWithoutFilter 101 | | InquirerQuestionWithoutFilter 102 | | InquirerQuestionWithoutFilter 103 | | InquirerQuestionWithoutFilter 104 | | InquirerQuestionWithoutFilter 105 | | InquirerQuestionWithoutFilter; 106 | 107 | export type HelpOptions = 'before' | 'beforeAll' | 'after' | 'afterAll'; 108 | -------------------------------------------------------------------------------- /apps/docs/src/pages/en/features/factory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CommandFactory 3 | layout: ../../../layouts/MainLayout.astro 4 | --- 5 | 6 | Okay, so you've got this fancy command set up, it takes in some user input, and 7 | is ready to go, but how do you start the CLI application? Well, just like in a 8 | Nest application where you can use `NestFactory.create()`, `nest-commander` 9 | comes with it's own `CommandFactory.run()` method. So let's wire everything up, 10 | set up the `main.ts` and see how this all works together. 11 | 12 | ## Registering Your Commands and Questions 13 | 14 | You may have noticed in the [Inquirer](./inquirer) section a quick mention of 15 | adding the question set class to the `providers`. In fact, both command classes 16 | and question set classes are nothing more than specialized providers! Due to 17 | this, we can simply add these classes to a module's metadata and make sure that 18 | module is in the root module the `CommandFactory` uses. 19 | 20 | ```typescript title="src/app.module.ts" 21 | @Module({ 22 | providers: [TaskRunner, TaskQuestions] 23 | }) 24 | export class AppModule {} 25 | ``` 26 | 27 | Do note that these providers do not need to be in the root module, nor do they 28 | need to be added to the `exports` array, unless they are injected elsewhere. Now 29 | with the `AppModule` set up, we can create the `main.ts` with the 30 | `CommandFactory`. 31 | 32 | ```typescript title="src/main.ts" 33 | const bootstrap = async () => { 34 | await CommandFactory.run(AppModule); 35 | }; 36 | 37 | bootstrap(); 38 | ``` 39 | 40 | And just like that, the command is hooked up and will run. You can use 41 | `typescript`, the NestJS CLI, or `ts-node` to compile and run the `dist/main.js` 42 | file (or `src/main.ts` in the case of `ts-node`). For a more in depth 43 | explanation on how to run the newly created commands, it is encouraged you check 44 | out the `Execution` portion of the docs. 45 | 46 | ## Logging 47 | 48 | By default, the `CommandFactory` turns **off** the Nest logger, due to the noise 49 | that the Nest logs create on startup with all of the modules and dependencies 50 | being resolved. If you'd like to turn logging back on, simple pass a valid 51 | logger configuration to the `CommandFactory` as a second parameter (e.g.: 52 | `new Logger()` from `@nestjs/common`). 53 | 54 | ## Error Handling 55 | 56 | By default, there is no error handler for commander provided by 57 | `nest-commander`. If there's a problem, it will fall back to commander's default 58 | functionality, which is to print the help information. If you want to provide 59 | your own handler though, simply pass an object with the `errorHandler` property 60 | that is a function taking in an `error` and returning `void`. 61 | 62 | ## Indefinite Running 63 | 64 | The `CommandFactory` also allows you to set up an infinite runner, so that you 65 | can set up file watchers or similar. All you need to do is instead of using 66 | `run` use `runWithoutClosing`. All other options are the same. 67 | 68 | For more information on the `CommandFactory`, please refer to the 69 | [API docs](../api#commandfactory). 70 | 71 | ## Creating an Application Without Running It 72 | 73 | There may come a time where you want to create a CLI application but not 74 | immediately run it, like wanting to use `app.useLogger()` to change the logger 75 | to one created by the DI process. You can achieve this by using 76 | `CommandFactory.createWithoutRunning()` using the same configuration that would 77 | be passed to `CommandFactory.run()`. This will create an 78 | `INestApplicationContext` for you without having to worry about all the internal 79 | modules and configuration used. To run the application later, simple call 80 | `await CommandFactory.runApplication(app)`. 81 | -------------------------------------------------------------------------------- /integration/plugins/test/plugin.command.spec.ts: -------------------------------------------------------------------------------- 1 | import { restore, stubMethod } from 'hanbi'; 2 | import { CommandFactory } from 'nest-commander'; 3 | import { join } from 'path'; 4 | import { suite } from 'uvu'; 5 | import { equal, match, not } from 'uvu/assert'; 6 | import { FooModule } from '../src/foo.module'; 7 | 8 | const setArgv = (command: string, file = 'plugin.command.js') => { 9 | process.argv = [process.argv0, join(__dirname, file), command]; 10 | }; 11 | 12 | export const PluginSuite = suite('Plugin Command Suite'); 13 | PluginSuite.after.each(() => { 14 | restore(); 15 | }); 16 | PluginSuite('command from the main module should work', async () => { 17 | const logSpy = stubMethod(console, 'log'); 18 | setArgv('phooey', 'foo.command.js'); 19 | await CommandFactory.run(FooModule); 20 | equal(logSpy.firstCall?.args[0], 'Foo!'); 21 | logSpy.restore(); 22 | }); 23 | PluginSuite('command from the plugin module should be available', async () => { 24 | const logSpy = stubMethod(console, 'log'); 25 | setArgv('plug'); 26 | const cwdSpy = stubMethod(process, 'cwd'); 27 | cwdSpy.returns(join(__dirname, '..')); 28 | await CommandFactory.run(FooModule, { usePlugins: true }); 29 | equal(logSpy.firstCall?.args[0], 'This is from the plugin!'); 30 | logSpy.restore(); 31 | }); 32 | PluginSuite('a custom config file should be allowed', async () => { 33 | const logSpy = stubMethod(console, 'log'); 34 | setArgv('plug'); 35 | const cwdSpy = stubMethod(process, 'cwd'); 36 | cwdSpy.returns(join(__dirname, '..')); 37 | await CommandFactory.run(FooModule, { 38 | usePlugins: true, 39 | cliName: 'custom-name', 40 | }); 41 | equal(logSpy.firstCall?.args[0], 'This is from the plugin!'); 42 | logSpy.restore(); 43 | }); 44 | /** 45 | * Cosmiconfig uses `process.cwd()` as a basis for a search directory to find the config files 46 | * during tests, `process.cwd()` results in ~/ which doesn't have the config files, so we set it 47 | * to result to ~/integration/plugins so that the configuration files can be found. If the config 48 | * file is not found, there will be an error about it. If the config file is found, but the command 49 | * requested is not a known command, there will be an error, but not about the config file, it will 50 | * just be assumed to be an unknown command 51 | */ 52 | PluginSuite( 53 | 'an error message should be written about not finding the config file', 54 | async () => { 55 | const errSpy = stubMethod(process.stderr, 'write'); 56 | const exitSpy = stubMethod(process, 'exit'); 57 | setArgv('foo'); 58 | try { 59 | await CommandFactory.run(FooModule, { usePlugins: true }); 60 | } finally { 61 | equal(exitSpy.firstCall?.args[0], 1); 62 | match( 63 | errSpy.getCall(1).args[0].toString() ?? '', 64 | "nest-commander is expecting a configuration file, but didn't find one. Are you in the right directory?", 65 | ); 66 | } 67 | }, 68 | ); 69 | PluginSuite( 70 | 'an error message should be shown for the unknown command, but not about a missing config file', 71 | async () => { 72 | const errSpy = stubMethod(process.stderr, 'write'); 73 | const exitSpy = stubMethod(process, 'exit'); 74 | setArgv('bar'); 75 | const cwdSpy = stubMethod(process, 'cwd'); 76 | cwdSpy.returns(join(__dirname, '..')); 77 | try { 78 | await CommandFactory.run(FooModule, { usePlugins: true }); 79 | } finally { 80 | console.log(errSpy); 81 | equal(exitSpy.firstCall?.args[0], 1); 82 | not.match( 83 | errSpy.firstCall?.args[0].toString() ?? '', 84 | "nest-commander is expecting a configuration file, but didn't find one. Are you in the right directory?", 85 | ); 86 | } 87 | }, 88 | ); 89 | -------------------------------------------------------------------------------- /packages/nest-commander/src/command.decorators.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Type } from '@nestjs/common'; 2 | import { 3 | CommandMetadata, 4 | CommandRunner, 5 | HelpOptions, 6 | OptionChoiceForMetadata, 7 | OptionMetadata, 8 | QuestionMetadata, 9 | QuestionNameMetadata, 10 | RootCommandMetadata, 11 | } from './command-runner.interface'; 12 | import { 13 | ChoicesMeta, 14 | Commander, 15 | CommandMeta, 16 | DefaultMeta, 17 | HelpMeta, 18 | MessageMeta, 19 | OptionChoiceMeta, 20 | OptionMeta, 21 | QuestionMeta, 22 | QuestionSetMeta, 23 | RootCommandMeta, 24 | SubCommandMeta, 25 | TransformMeta, 26 | ValidateMeta, 27 | WhenMeta, 28 | } from './constants'; 29 | 30 | type CommandDecorator = >( 31 | target: TFunction, 32 | ) => void | TFunction; 33 | 34 | const applyMethodMetadata = ( 35 | options: any, 36 | metadataKey: string, 37 | ): MethodDecorator => { 38 | return ( 39 | _target: Record, 40 | _propertyKey: string | symbol, 41 | descriptor: PropertyDescriptor, 42 | ) => { 43 | Reflect.defineMetadata(metadataKey, options, descriptor.value); 44 | return descriptor; 45 | }; 46 | }; 47 | 48 | const applyClassMetadata = ( 49 | options: any, 50 | metadataKey: string, 51 | ): ClassDecorator => { 52 | return (target) => { 53 | Reflect.defineMetadata(metadataKey, options, target); 54 | return target; 55 | }; 56 | }; 57 | export const Command = (options: CommandMetadata): CommandDecorator => { 58 | return applyClassMetadata(options, CommandMeta); 59 | }; 60 | 61 | export const SubCommand = (options: CommandMetadata): CommandDecorator => { 62 | return applyClassMetadata(options, SubCommandMeta); 63 | }; 64 | 65 | export const RootCommand = (options: RootCommandMetadata): CommandDecorator => { 66 | return applyClassMetadata(options, RootCommandMeta); 67 | }; 68 | 69 | export const DefaultCommand = RootCommand; 70 | 71 | export const Option = (options: OptionMetadata): MethodDecorator => { 72 | return applyMethodMetadata(options, OptionMeta); 73 | }; 74 | 75 | export const OptionChoiceFor = ( 76 | options: OptionChoiceForMetadata, 77 | ): MethodDecorator => { 78 | return applyMethodMetadata(options, OptionChoiceMeta); 79 | }; 80 | 81 | export const QuestionSet = (options: QuestionNameMetadata): ClassDecorator => { 82 | return applyClassMetadata(options, QuestionSetMeta); 83 | }; 84 | 85 | export const Question = (options: QuestionMetadata): MethodDecorator => { 86 | return applyMethodMetadata(options, QuestionMeta); 87 | }; 88 | 89 | export const ValidateFor = (options: QuestionNameMetadata): MethodDecorator => { 90 | return applyMethodMetadata(options, ValidateMeta); 91 | }; 92 | 93 | export const TransformFor = ( 94 | options: QuestionNameMetadata, 95 | ): MethodDecorator => { 96 | return applyMethodMetadata(options, TransformMeta); 97 | }; 98 | 99 | export const WhenFor = (options: QuestionNameMetadata): MethodDecorator => { 100 | return applyMethodMetadata(options, WhenMeta); 101 | }; 102 | 103 | export const MessageFor = (options: QuestionNameMetadata): MethodDecorator => { 104 | return applyMethodMetadata(options, MessageMeta); 105 | }; 106 | 107 | export const ChoicesFor = (options: QuestionNameMetadata): MethodDecorator => { 108 | return applyMethodMetadata(options, ChoicesMeta); 109 | }; 110 | 111 | export const DefaultFor = (options: QuestionNameMetadata): MethodDecorator => { 112 | return applyMethodMetadata(options, DefaultMeta); 113 | }; 114 | 115 | export const Help = (options: HelpOptions): MethodDecorator => { 116 | return applyMethodMetadata(options, HelpMeta); 117 | }; 118 | 119 | export const InjectCommander = () => Inject(Commander); 120 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - 'main' 10 | schedule: 11 | - cron: '0 0 * * *' 12 | 13 | env: 14 | NX_CLOUD_DISTRIBUTED_EXECUTION: ${{ !contains(github.event.pull_request.user.login, 'dependabot') && !contains(github.event.pull_request.user.login, 'renovate') }} 15 | NX_CLOUD_AUTH_TOKEN: ${{ startsWith(github.repository, 'jmcdo29') && secrets.NX_CLOUD_TOKEN || 'OTRjNTE0ZTgtNmVjOC00NjNmLWFkYzctYWM0MDlhM2VmNzMyfHJlYWQ=' }} 16 | 17 | jobs: 18 | main: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.event_name != 'pull_request' }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | name: Checkout [main] 24 | with: 25 | fetch-depth: 0 26 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 27 | uses: nrwl/nx-set-shas@v4 28 | - name: Setup 29 | uses: ./.github/actions/setup 30 | - name: Lint, Build, Test 31 | uses: ./.github/actions/lint-build-test 32 | - name: Stop Nx Cloud Agents 33 | run: pnpm nx-cloud stop-all-agents 34 | - name: Tag main branch if all jobs succeed 35 | uses: nrwl/nx-tag-successful-ci-run@v1 36 | pr: 37 | runs-on: ubuntu-latest 38 | if: ${{ github.event_name == 'pull_request' }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | with: 42 | # ref: ${{ github.event.pull_request.head.ref }} 43 | fetch-depth: 0 44 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 45 | uses: nrwl/nx-set-shas@v4 46 | - name: Setup 47 | uses: ./.github/actions/setup 48 | - name: Lint, Build, Test 49 | uses: ./.github/actions/lint-build-test 50 | - name: Stop Nx Cloud Agents 51 | run: pnpm nx-cloud stop-all-agents 52 | 53 | analyze: 54 | name: Analyze 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | with: 61 | fetch-depth: 0 62 | 63 | - name: Initialize CodeQL 64 | uses: github/codeql-action/init@v3 65 | with: 66 | languages: javascript 67 | 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v3 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | 74 | auto-merge: 75 | needs: pr 76 | if: contains(github.event.pull_request.user.login, 'dependabot') || contains(github.event.pull_request.user.login, 'renovate') 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: automerge 80 | uses: pascalgn/automerge-action@v0.16.4 81 | env: 82 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 83 | MERGE_LABELS: '' 84 | MERGE_METHOD: rebase 85 | 86 | send-coverage: 87 | runs-on: ubuntu-latest 88 | needs: [pr, main] 89 | if: always() 90 | steps: 91 | - name: Download Coverage 92 | uses: actions/download-artifact@v4 93 | with: 94 | name: coverage 95 | path: coverage/ 96 | 97 | - name: Send Coverage 98 | run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r coverage/lcov.info 99 | shell: bash 100 | env: 101 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 102 | 103 | agents: 104 | runs-on: ubuntu-latest 105 | name: Nx Agent 106 | strategy: 107 | matrix: 108 | agent: [1, 2, 3] 109 | steps: 110 | - uses: actions/checkout@v4 111 | - name: Setup 112 | uses: ./.github/actions/setup 113 | - name: Start Nx Agent ${{ matrix.agent }} 114 | run: pnpm nx-cloud start-agent 115 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Reporting Issues 4 | 5 | If you find an issue, please 6 | [report it here](https://github.com/jmcdo29/nest-commander/issues/new/choose). Follow the issue 7 | template and please fill out the information. Especially if it is a bug, please provide a 8 | [minimum reproduction repository](https://minimum-reproduction.wtf). 9 | 10 | ## Pre-Requisites 11 | 12 | - [pnpm installed](https://pnpm.io) (you can try using `yarn` or `npm`, but sometimes they don't 13 | play nicely with the pnpm workspace) 14 | - [Basic knowledge of Nx](https://nx.dev) 15 | 16 | ### Getting Started 17 | 18 | The following steps are all it should really take to get started 19 | 20 | 1. Create a fork of the project 21 | 2. `git clone ` 22 | 3. `cd nest-commander` 23 | 4. `pnpm i` 24 | 25 | And now you should be good to go 26 | 27 | ### Project Structure 28 | 29 | `nest-commander` uses an [`nx`](https://nx.dev) workspace for development. This means that the 30 | packages can be found under `packages/` and each package has its own set of commands that can be 31 | found in the [`workspace.json`](./workspace.json) file. All of the docs for `nest-commander` can be 32 | found at `apps/docs/docs` and are all markdown files [thanks to docusaurus](https://docusaurus.io/). 33 | All integration tests can be found at `integration/` 34 | 35 | ### Making changes 36 | 37 | Generally changes and improvements are appreciated, especially if they make logic less complex or 38 | they end up causing [codebeat](https://codebeat.co/) report a major (greater than .2) loss in code 39 | GPA. Other than that, follow the lint rules set up in the project, and make sure the git hooks run 40 | before [opening a Pull Request](https://github.com/jmcdo29/nest-commander/compare). Also, make sure 41 | if it's a new feature or a bug fix that a test is added to the integration tests. Lastly, please 42 | resolve any merge conflicts by [rebasing](https://git-scm.com/book/en/v2/Git-Branching-Rebasing). 43 | The easiest way to go about this is to use `git pull --rebase upstream main` where `upstream` is a 44 | remote set to `https://github.com/jmcdo29/nest-commander.git` or 45 | `git@github.com:jmcdo29/nest-commander.git` depending on if you use HTTPS or SSH with your git 46 | client. 47 | 48 | #### Adding a Changeset 49 | 50 | If you're making a change that you'd like to be published, please consider running `pnpm changeset` 51 | and following the wizard for setting up a changeset for the change. When this gets merged into 52 | `main`, the GitHub Actions will end up opening a new PR afterwards for updating the version and 53 | publishing to npm. 54 | [If you're interested in how, there's a blog post about it here](https://dev.to/jmcdo29/automating-your-package-deployment-in-an-nx-monorepo-with-changeset-4em8). 55 | The wizard is pretty straight forward, I do ask that you try to follow [semver](https://semver.org/) 56 | as much as possible and don't make major changes unless absolutely necessary. 57 | 58 | ### Building just one project 59 | 60 | If you need to just build a single project, you can use `pnpm nx build `. If you want 61 | to build everything that has been affected you can instead use `pnpm build` which will use 62 | `nx affected:build` instead. 63 | 64 | ### Testing just one suite 65 | 66 | To run the tests, you can use `pnpm e2e` or `pnpm nx e2e integration`, this will run all of the 67 | integration tests, it shouldn't take more than 15 seconds. If it does, there's most likely an `exit` 68 | call that was mocked and not restored. Ping me on discord and we can try to find it. 69 | 70 | If you need to just run one of the test suites you can use `pnpm e2e -- --testFile=`. 71 | 72 | ## Keeping in touch 73 | 74 | You can get a hold of me (Jay) by [emailing me](mailto:me@jaymcdoniel.dev) or by contacting me on 75 | [the Nest Discord](https://discord.gg/6byqVsXzaF) (there's a channel dedicated to `nest-commander`) 76 | -------------------------------------------------------------------------------- /packages/nest-commander-testing/src/command-test.factory.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing'; 3 | import { randomBytes } from 'crypto'; 4 | import { Answers, DistinctQuestion, ListQuestion } from 'inquirer'; 5 | import { 6 | CommandRunnerModule, 7 | CommandRunnerService, 8 | Inquirer, 9 | } from 'nest-commander'; 10 | import { CommanderOptionsType } from 'nest-commander'; 11 | 12 | export type CommandModuleMetadata = Exclude & { 13 | imports: NonNullable; 14 | }; 15 | 16 | export class CommandTestFactory { 17 | private static testAnswers = []; 18 | private static useOriginalInquirer = false; 19 | 20 | static useDefaultInquirer() { 21 | this.useOriginalInquirer = true; 22 | return this; 23 | } 24 | static createTestingCommand( 25 | moduleMetadata: CommandModuleMetadata, 26 | options?: CommanderOptionsType, 27 | ): TestingModuleBuilder { 28 | moduleMetadata.imports.push( 29 | CommandRunnerModule.forModule(undefined, options), 30 | ); 31 | const testingModule = Test.createTestingModule(moduleMetadata); 32 | if (!this.useOriginalInquirer) { 33 | testingModule.overrideProvider(Inquirer).useValue({ 34 | prompt: this.promptMock.bind(this), 35 | }); 36 | } 37 | return testingModule; 38 | } 39 | 40 | private static async promptMock( 41 | questions: ReadonlyArray, 42 | answers: Answers = {}, 43 | ) { 44 | for (let i = 0; i < questions.length; i++) { 45 | const question = questions[i]; 46 | if ((question.name && answers[question.name]) || !this.testAnswers[i]) { 47 | continue; 48 | } 49 | let answer; 50 | if (question.validate) { 51 | await question.validate(this.testAnswers[i]); 52 | } 53 | if (question.when && typeof question.when === 'function') { 54 | await question.when(answers); 55 | } 56 | if ((question as ListQuestion).choices) { 57 | let choices = (question as ListQuestion).choices; 58 | if (typeof choices === 'function') { 59 | choices = await choices(answers); 60 | } 61 | const choice = ( 62 | choices as Array<{ key?: string; value?: string; name?: string }> 63 | ).find((c) => c.key === this.testAnswers[i]); 64 | answer = choice?.value || this.testAnswers[i]; 65 | } else { 66 | answer = this.testAnswers[i]; 67 | } 68 | if (question.default && typeof question.default === 'function') { 69 | await question.default(this.testAnswers); 70 | } 71 | if (question.message && typeof question.message === 'function') { 72 | await question.message(this.testAnswers); 73 | } 74 | answers[question.name ?? 'default'] = 75 | (await question.filter?.(answer, answers)) ?? answer; 76 | } 77 | return answers; 78 | } 79 | 80 | static async run(app: TestingModule, args: string[] = []) { 81 | const application = await this.runApplication(app, args); 82 | await application.close(); 83 | } 84 | 85 | static async runWithoutClosing(app: TestingModule, args: string[] = []) { 86 | return this.runApplication(app, args); 87 | } 88 | 89 | private static async runApplication(app: TestingModule, args: string[] = []) { 90 | if (args?.length && args[0] !== 'node') { 91 | args = ['node', randomBytes(8).toString('hex') + '.js'].concat(args); 92 | } 93 | await app.init(); 94 | const runner = app.get(CommandRunnerService); 95 | await runner.run(args); 96 | return app; 97 | } 98 | 99 | static setAnswers(value: any | any[]): void { 100 | if (!Array.isArray(value)) { 101 | value = [value]; 102 | } 103 | this.testAnswers = value; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /apps/docs/src/layouts/MainLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import HeadCommon from '../components/HeadCommon.astro'; 3 | import HeadSEO from '../components/HeadSEO.astro'; 4 | import Header from '../components/Header/Header.astro'; 5 | import PageContent from '../components/PageContent/PageContent.astro'; 6 | import LeftSidebar from '../components/LeftSidebar/LeftSidebar.astro'; 7 | import RightSidebar from '../components/RightSidebar/RightSidebar.astro'; 8 | import * as CONFIG from '../config'; 9 | import type { MarkdownHeading } from 'astro'; 10 | import Footer from '../components/Footer/Footer.astro'; 11 | 12 | type Props = { 13 | frontmatter: CONFIG.Frontmatter; 14 | headings: MarkdownHeading[]; 15 | }; 16 | 17 | const { frontmatter, headings } = Astro.props as Props; 18 | const canonicalURL = new URL(Astro.url.pathname, Astro.site); 19 | const currentPage = Astro.url.pathname; 20 | const currentFile = `src/pages${currentPage.replace(/\/$/, '')}.md`; 21 | const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`; 22 | --- 23 | 24 | 25 | 26 | 27 | 28 | 29 | {frontmatter.title ? `${frontmatter.title} | ${CONFIG.SITE.title}` : CONFIG.SITE.title} 30 | 31 | 105 | 120 | 121 | 122 | 123 |
124 |
125 | 128 |
129 | 130 | 131 | 132 |
133 | 136 |
137 |