├── .github ├── CODEOWNERS ├── CONTRIBUTING.md └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── pr_labels.yml │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TRADEMARK.txt ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── blog │ ├── 2024-10-01-introducing-stricli.mdx │ └── authors.yml ├── docs │ ├── features │ │ ├── _category_.json │ │ ├── argument-parsing │ │ │ ├── _category_.json │ │ │ ├── examples │ │ │ │ ├── array-argument.txt │ │ │ │ ├── boolean-flag.txt │ │ │ │ ├── bounded-array-argument.txt │ │ │ │ ├── counter-flag.txt │ │ │ │ ├── default-flag.txt │ │ │ │ ├── default-tuple-argument.txt │ │ │ │ ├── enum-flag.txt │ │ │ │ ├── hidden-flag.txt │ │ │ │ ├── optional-flag.txt │ │ │ │ ├── optional-tuple-argument.txt │ │ │ │ ├── parsed-flag.txt │ │ │ │ ├── placeholder-argument.txt │ │ │ │ ├── tuple-argument.txt │ │ │ │ └── variadic-flag.txt │ │ │ ├── flags.mdx │ │ │ ├── index.mdx │ │ │ └── positional.mdx │ │ ├── command-routing │ │ │ ├── _category_.json │ │ │ ├── commands.mdx │ │ │ ├── index.mdx │ │ │ └── route-maps.mdx │ │ ├── configuration.mdx │ │ ├── isolated-context.mdx │ │ ├── out-of-scope.mdx │ │ └── shell-autocomplete.mdx │ ├── getting-started │ │ ├── _category_.json │ │ ├── alternatives.mdx │ │ ├── faq.mdx │ │ ├── overview.mdx │ │ └── principles.mdx │ ├── quick-start.mdx │ ├── sidebars.ts │ └── testing.mdx ├── docusaurus.config.ts ├── eslint.config.mjs ├── package.json ├── packages │ ├── index.mdx │ └── sidebars.ts ├── src │ ├── components │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── StricliPlayground │ │ │ ├── Editor │ │ │ ├── MonacoContainer.tsx │ │ │ ├── index.tsx │ │ │ └── themes │ │ │ │ ├── dark.json │ │ │ │ └── light.json │ │ │ ├── Terminal │ │ │ ├── Ansi.tsx │ │ │ └── index.tsx │ │ │ ├── hooks.ts │ │ │ ├── impl.tsx │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.scss │ ├── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md │ └── util │ │ └── array.ts ├── static │ ├── .nojekyll │ └── img │ │ ├── S-logo.ico │ │ ├── S-logo.svg │ │ └── intro │ │ ├── AutocompleteSupport.svg │ │ ├── DeriveParsingFromArgumentsViaTypes.svg │ │ ├── ExplicitCommandRouting.svg │ │ └── SplitDefinitionFromImplementation.svg └── tsconfig.json ├── eslint.config.mjs ├── examples ├── bun │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── bin │ │ │ └── cli.ts │ │ └── commands │ │ │ ├── echo.ts │ │ │ └── math.ts │ └── tsconfig.json ├── deno │ ├── README.md │ ├── package.json │ └── src │ │ ├── app.ts │ │ ├── bin │ │ └── cli.ts │ │ └── commands │ │ ├── echo.ts │ │ └── math.ts └── node │ ├── README.md │ ├── package.json │ ├── src │ ├── app.ts │ ├── bin │ │ └── cli.ts │ └── commands │ │ ├── echo.ts │ │ └── math.ts │ └── tsconfig.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── auto-complete │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── src │ │ ├── app.ts │ │ ├── bin │ │ │ └── cli.ts │ │ ├── commands.ts │ │ ├── context.ts │ │ ├── formatting.ts │ │ ├── impl.ts │ │ ├── index.ts │ │ ├── shells │ │ │ └── bash.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── core │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── scripts │ │ ├── accept_baseline.js │ │ └── clear_baseline.js │ ├── src │ │ ├── application │ │ │ ├── builder.ts │ │ │ ├── documentation.ts │ │ │ ├── propose-completions.ts │ │ │ ├── run.ts │ │ │ └── types.ts │ │ ├── config.ts │ │ ├── context.ts │ │ ├── exit-code.ts │ │ ├── index.ts │ │ ├── parameter │ │ │ ├── flag │ │ │ │ ├── formatting.ts │ │ │ │ └── types.ts │ │ │ ├── formatting.ts │ │ │ ├── parser │ │ │ │ ├── boolean.ts │ │ │ │ ├── choice.ts │ │ │ │ └── number.ts │ │ │ ├── positional │ │ │ │ ├── formatting.ts │ │ │ │ └── types.ts │ │ │ ├── scanner.ts │ │ │ └── types.ts │ │ ├── routing │ │ │ ├── command │ │ │ │ ├── builder.ts │ │ │ │ ├── documentation.ts │ │ │ │ ├── propose-completions.ts │ │ │ │ ├── run.ts │ │ │ │ └── types.ts │ │ │ ├── route-map │ │ │ │ ├── builder.ts │ │ │ │ ├── documentation.ts │ │ │ │ ├── propose-completions.ts │ │ │ │ └── types.ts │ │ │ ├── scanner.ts │ │ │ └── types.ts │ │ ├── text.ts │ │ ├── tsconfig.json │ │ └── util │ │ │ ├── array.ts │ │ │ ├── case-style.ts │ │ │ ├── distance.ts │ │ │ ├── error.ts │ │ │ ├── formatting.ts │ │ │ └── promise.ts │ ├── tests │ │ ├── application.spec.ts │ │ ├── baseline.ts │ │ ├── baselines │ │ │ └── reference │ │ │ │ ├── application.txt │ │ │ │ ├── parameter │ │ │ │ ├── flag │ │ │ │ │ └── formatting.txt │ │ │ │ ├── formatting.txt │ │ │ │ └── positional │ │ │ │ │ └── formatting.txt │ │ │ │ └── routing │ │ │ │ ├── command.txt │ │ │ │ └── route-map.txt │ │ ├── fakes │ │ │ ├── config.ts │ │ │ └── context.ts │ │ ├── parameter │ │ │ ├── flag │ │ │ │ └── formatting.spec.ts │ │ │ ├── formatting.spec.ts │ │ │ ├── parser.ts │ │ │ ├── parsers │ │ │ │ ├── boolean.spec.ts │ │ │ │ ├── choice.spec.ts │ │ │ │ └── number.spec.ts │ │ │ ├── positional │ │ │ │ └── formatting.spec.ts │ │ │ └── scanner.spec.ts │ │ ├── routing │ │ │ ├── command.spec.ts │ │ │ ├── route-map.spec.ts │ │ │ └── scanner.spec.ts │ │ ├── type-inference.spec.ts │ │ └── util │ │ │ ├── distance.spec.ts │ │ │ └── formatting.spec.ts │ └── tsconfig.json └── create-app │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── scripts │ ├── accept_baseline.js │ └── clear_baseline.js │ ├── src │ ├── app.ts │ ├── bin │ │ └── cli.ts │ ├── context.ts │ ├── files.ts │ ├── impl.ts │ ├── node.ts │ ├── registry.ts │ └── tsconfig.json │ ├── tests │ ├── app.spec.ts │ ├── baseline.ts │ ├── baselines │ │ └── reference │ │ │ └── app │ │ │ └── creates new application │ │ │ ├── checks for @types__node │ │ │ ├── node version logic │ │ │ │ ├── exact version exists for types.txt │ │ │ │ ├── major version does not exist in registry, picks highest even major.txt │ │ │ │ ├── major version exists in registry.txt │ │ │ │ └── version discovery skipped when --node-version is provided.txt │ │ │ └── registry logic │ │ │ │ ├── NPM_EXECPATH throws an error.txt │ │ │ │ ├── no check for safe major version.txt │ │ │ │ ├── reads registry direct from NPM_CONFIG_REGISTRY, URL ends with slash.txt │ │ │ │ ├── reads registry direct from NPM_CONFIG_REGISTRY.txt │ │ │ │ ├── registry data has no versions.txt │ │ │ │ ├── request to registry throws error.txt │ │ │ │ ├── unable to discover registry from process.txt │ │ │ │ └── uses NPM_EXECPATH to get registry config value.txt │ │ │ ├── multi-command │ │ │ ├── commonjs │ │ │ │ ├── additional features │ │ │ │ │ └── without auto-complete.txt │ │ │ │ ├── package properties │ │ │ │ │ ├── custom bin command.txt │ │ │ │ │ ├── custom metadata.txt │ │ │ │ │ ├── custom name and bin command.txt │ │ │ │ │ └── custom name.txt │ │ │ │ └── with default flags.txt │ │ │ └── module [default] │ │ │ │ ├── additional features │ │ │ │ └── without auto-complete.txt │ │ │ │ ├── package properties │ │ │ │ ├── custom bin command.txt │ │ │ │ ├── custom metadata.txt │ │ │ │ ├── custom name and bin command.txt │ │ │ │ └── custom name.txt │ │ │ │ └── with default flags.txt │ │ │ └── single-command │ │ │ ├── commonjs │ │ │ ├── additional features │ │ │ │ └── without auto-complete.txt │ │ │ ├── package properties │ │ │ │ ├── custom bin command.txt │ │ │ │ ├── custom metadata.txt │ │ │ │ ├── custom name and bin command.txt │ │ │ │ └── custom name.txt │ │ │ └── with default flags.txt │ │ │ └── module [default] │ │ │ ├── additional features │ │ │ └── without auto-complete.txt │ │ │ ├── package properties │ │ │ ├── custom bin command.txt │ │ │ ├── custom metadata.txt │ │ │ ├── custom name and bin command.txt │ │ │ └── custom name.txt │ │ │ └── with default flags.txt │ ├── stream.ts │ └── types.ts │ └── tsconfig.json └── scripts └── add_labels_to_pr.sh /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bloomberg/stricli-devs 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | We welcome your contributions to help us improve and extend this project! 4 | 5 | Below you will find some basic steps required to be able to contribute to the project. If 6 | you have any questions about this process or any other aspect of contributing to a Bloomberg open 7 | source project, feel free to send an email to opensource@bloomberg.net and we'll get your questions 8 | answered as quickly as we can. 9 | 10 | ## Contribution Licensing 11 | 12 | Since this project is distributed under the terms of an [open source license](../LICENSE), contributions that you make 13 | are licensed under the same terms. In order for us to be able to accept your contributions, 14 | we will need explicit confirmation from you that you are able and willing to provide them under 15 | these terms, and the mechanism we use to do this is called a Developer's Certificate of Origin 16 | [(DCO)](https://github.com/bloomberg/.github/blob/main/DCO.md). This is very similar to the process used by the Linux(R) kernel, Samba, and many 17 | other major open source projects. 18 | 19 | To participate under these terms, all that you must do is include a line like the following as the 20 | last line of the commit message for each commit in your contribution: 21 | 22 | Signed-Off-By: Random J. Developer 23 | 24 | The simplest way to accomplish this is to add `-s` or `--signoff` to your `git commit` command. 25 | 26 | You must use your real name (sorry, no pseudonyms, and no anonymous contributions). 27 | 28 | ## Steps 29 | 30 | - Create an Issue, selecting 'Feature Request', and explain the proposed change. 31 | - Follow the guidelines in the issue template presented to you. 32 | - Submit the Issue. 33 | - Submit a Pull Request and link it to the Issue by including "#" in the Pull Request summary. 34 | 35 | ## Help / Documentation 36 | 37 | Please see the project's README to get started. 38 | 39 | ## Code of Conduct 40 | 41 | This project has adopted a [Code of Conduct](https://github.com/bloomberg/.github/blob/main/CODE_OF_CONDUCT.md). If you have any concerns about the Code, or behavior 42 | which you have experienced in the project, please contact us at opensource@bloomberg.net. 43 | 44 | ## Security Vulnerability Reporting 45 | 46 | If you believe you have identified a security vulnerability in this project, please send email to the project 47 | team at opensource@bloomberg.net, detailing the suspected issue and any methods you've found to reproduce it. 48 | 49 | Please do NOT open an issue in the GitHub repository, as we'd prefer to keep vulnerability reports private until 50 | we've had an opportunity to review and address them. 51 | 52 | ## Licensing 53 | 54 | See the LICENSE file in the top directory of the project repository for licensing information about the project. 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | actions: read 13 | contents: read 14 | 15 | jobs: 16 | nx: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | cache: "npm" 27 | 28 | - run: npm ci --legacy-peer-deps 29 | - uses: nrwl/nx-set-shas@v4 30 | 31 | - run: npx nx run-many -t format:check lint build coverage 32 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build Docusaurus 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | cache: npm 20 | 21 | - name: Install dependencies 22 | run: npm ci --legacy-peer-deps 23 | - name: Build website 24 | run: npx nx run @stricli/docs:build-docs 25 | 26 | - name: Upload Build Artifact 27 | uses: actions/upload-pages-artifact@v3 28 | with: 29 | path: docs/build 30 | 31 | deploy: 32 | name: Deploy to GitHub Pages 33 | needs: build 34 | 35 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 36 | permissions: 37 | pages: write # to deploy to Pages 38 | id-token: write # to verify the deployment originates from an appropriate source 39 | 40 | # Deploy to the github-pages environment 41 | environment: 42 | name: github-pages 43 | url: ${{ steps.deployment.outputs.page_url }} 44 | 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /.github/workflows/pr_labels.yml: -------------------------------------------------------------------------------- 1 | name: Add labels to PR 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | pr-labels: 12 | # Only run on local PRs, not forks 13 | if: github.event.pull_request.head.repo.full_name == github.repository 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - run: ./scripts/add_labels_to_pr.sh 23 | env: 24 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | GH_REPO: ${{ github.repository }} 26 | PR_NUMBER: ${{ github.event.pull_request.number }} 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Nx Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | dry-run: 10 | type: boolean 11 | required: true 12 | default: true 13 | description: Trigger publish as a dry run, so no packages are published. 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | id-token: write # needed for provenance data generation 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | fetch-tags: true 25 | 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: "npm" 30 | registry-url: "https://registry.npmjs.org/" 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | - run: npm ci --legacy-peer-deps 35 | - uses: nrwl/nx-set-shas@v4 36 | 37 | - run: npx nx release publish ${{ inputs.dry-run == 'true' && '--dry-run' || '' }} 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | NPM_CONFIG_PROVENANCE: true 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # TypeScript 107 | lib/ 108 | 109 | # esbuild 110 | build/ 111 | 112 | .turbo/ 113 | .nx/ 114 | 115 | # Dependencies 116 | /node_modules 117 | 118 | # Production 119 | /build 120 | 121 | # Generated files 122 | .docusaurus 123 | .cache-loader 124 | 125 | # Misc 126 | .DS_Store 127 | .env.local 128 | .env.development.local 129 | .env.test.local 130 | .env.production.local 131 | 132 | tests/baselines/local 133 | 134 | 135 | 136 | .nx/cache 137 | .nx/workspace-data 138 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.2 (2025-03-26) 2 | 3 | 4 | ### 🩹 Fixes 5 | 6 | - make looseBooleanParser even more permissive ([07d4bc5](https://github.com/bloomberg/stricli/commit/07d4bc5)) 7 | 8 | ### ❤️ Thank You 9 | 10 | - Michael Molisani 11 | 12 | ## 1.1.1 (2025-01-13) 13 | 14 | 15 | ### 🩹 Fixes 16 | 17 | - loosen character restrictons on flags ([2088ffd](https://github.com/bloomberg/stricli/commit/2088ffd)) 18 | - use exit code ([3ac7278](https://github.com/bloomberg/stricli/commit/3ac7278)) 19 | - use correct flag for postinstall auto-complete command ([b70bd39](https://github.com/bloomberg/stricli/commit/b70bd39)) 20 | 21 | ### ❤️ Thank You 22 | 23 | - Marty Jones 24 | - Michael Molisani 25 | 26 | ## 1.1.0 (2024-11-04) 27 | 28 | 29 | ### 🚀 Features 30 | 31 | - new option to display brief with custom usage ([d78fda3](https://github.com/bloomberg/stricli/commit/d78fda3)) 32 | 33 | ### 🩹 Fixes 34 | 35 | - verify @types/node version with registry ([1a762b0](https://github.com/bloomberg/stricli/commit/1a762b0)) 36 | - hidden --node-version flag to bypass version discovery ([f237e04](https://github.com/bloomberg/stricli/commit/f237e04)) 37 | 38 | ### ❤️ Thank You 39 | 40 | - Michael Molisani 41 | 42 | ## 1.0.1 (2024-10-22) 43 | 44 | 45 | ### 🩹 Fixes 46 | 47 | - update auto-complete template to output completions ([a79e59f](https://github.com/bloomberg/stricli/commit/a79e59f)) 48 | - export constituent types of TypedCommandParameters individually ([09dfb8d](https://github.com/bloomberg/stricli/commit/09dfb8d)) 49 | - allow flags to parse arrays without variadic modifier ([ecc9099](https://github.com/bloomberg/stricli/commit/ecc9099)) 50 | 51 | ### ❤️ Thank You 52 | 53 | - Michael Molisani 54 | 55 | # 1.0.0 (2024-10-01) 56 | 57 | 58 | ### 🩹 Fixes 59 | 60 | - **create-app:** add process to context ([0c502f2](https://github.com/bloomberg/stricli/commit/0c502f2)) 61 | 62 | ### ❤️ Thank You 63 | 64 | - Kubilay Kahveci 65 | 66 | ## 0.0.1 (2024-09-30) 67 | 68 | This was a version bump only, there were no code changes. 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stricli 2 | 3 | Build complex CLIs with type safety and no dependencies. 4 | 5 | 👉 See **[bloomberg.github.io/stricli](https://bloomberg.github.io/stricli/)** for documentation about this framework. 6 | 7 | ## Contents 8 | 9 | - [Rationale](#rationale) 10 | - [Quick Start](#quick-start) 11 | - [Building](#building) 12 | - [Installation](#installation) 13 | - [Contributions](#contributions) 14 | - [License](#license) 15 | - [Code of Conduct](#code-of-conduct) 16 | - [Security Vulnerability Reporting](#security-vulnerability-reporting) 17 | 18 | ## Rationale 19 | 20 | This framework was developed by Bloomberg after evaluating the [available alternatives](https://bloomberg.github.io/stricli/docs/getting-started/alternatives) and developing a set of [guiding principles](https://bloomberg.github.io/stricli/docs/getting-started/principles). 21 | 22 | ## Quick Start 23 | 24 | Check out [the quick start](https://bloomberg.github.io/stricli/docs/quick-start) to learn how to generate a new Stricli application. 25 | 26 | ## Installation 27 | 28 | The core Stricli framework is available on npmjs.com, and can be installed with the following command: 29 | 30 | ``` 31 | npm i --save-prod @stricli/core 32 | ``` 33 | 34 | ## Development 35 | 36 | Run `npm ci` to initialize the repo. We use Nx to manage tasks, so you can run the following to build all of the packages at once: 37 | 38 | ``` 39 | npx nx@latest run-many -t build 40 | ``` 41 | 42 | ## Contributions 43 | 44 | We :heart: contributions. 45 | 46 | Have you had a good experience with this project? Why not share some love and contribute code, or just let us know about any issues you had with it? 47 | 48 | We welcome issue reports [here](../../issues); be sure to choose the proper issue template for your issue, so that we can be sure you're providing the necessary information. 49 | 50 | Before sending a [Pull Request](../../pulls), please make sure you read our [Contribution Guidelines](./.github/CONTRIBUTING.md). 51 | 52 | ## License 53 | 54 | Please read the [LICENSE](LICENSE) file. 55 | 56 | ## Code of Conduct 57 | 58 | This project has adopted a [Code of Conduct](https://github.com/bloomberg/.github/blob/main/CODE_OF_CONDUCT.md). 59 | If you have any concerns about the Code, or behavior which you have experienced in the project, please 60 | contact us at opensource@bloomberg.net. 61 | 62 | ## Security Vulnerability Reporting 63 | 64 | Please refer to the project [Security Policy](https://github.com/bloomberg/.github/blob/main/SECURITY.MD). 65 | -------------------------------------------------------------------------------- /TRADEMARK.txt: -------------------------------------------------------------------------------- 1 | STRICLI Trademark Guidelines 2 | ============================== 3 | 4 | STRICLI and the STRICLI logos (the “STRICLI Marks”) are trademarks and 5 | service marks of Bloomberg L.P. (“Bloomberg”) and, as such, are symbols of the 6 | quality of the products and services that Bloomberg offers. These trademark 7 | guidelines are essential and must be followed to prevent use of the STRICLI 8 | Marks that could lead to confusion about our software or our company. 9 | 10 | The STRICLI Marks may be used in the following manners or places: 11 | 12 | - on your website or in your social media accounts in content related to the 13 | STRICLI project; 14 | 15 | - in connection with conferences, events or talks related to the STRICLI 16 | project; 17 | 18 | - in connection with educational services offered related to the STRICLI 19 | project; in the title of an article or book (print or digital) related to the 20 | STRICLI project; 21 | 22 | - in a video (title or content) related to the STRICLI project; 23 | 24 | - to indicate that your product is an ecosystem component meant to enhance 25 | STRICLI's software, e.g. stricli-[component name]. 26 | 27 | 28 | The STRICLI Marks may not be used: 29 | 30 | - on merchandise; 31 | - in or as part of your company name; 32 | - in conjunction with your or another company’s logo or mark; or 33 | - to disparage Bloomberg or its goods or services. 34 | 35 | By use of the STRICLI Marks, each individual or organization: 36 | 37 | - Acknowledges that Bloomberg is the owner of all right, title and interest in 38 | and to the STRICLI Marks and the goodwill of the business related thereto 39 | and that all use of the STRICLI Marks will inure solely to the benefit of 40 | Bloomberg. 41 | 42 | - Agrees not to: (i) apply anywhere for trademark or service mark registration 43 | of the STRICLI Marks or any mark including the STRICLI Marks; (ii) use 44 | the STRICLI Marks in any manner or commit any other act likely to devalue, 45 | injure or dilute the goodwill or reputation of Bloomberg or the STRICLI 46 | Marks; or (iii) challenge the validity of the STRICLI Marks. 47 | 48 | - Acknowledges that Bloomberg will exercise control over the quality of use of 49 | the STRICLI Marks and the goods/services provided thereunder and will: (i) 50 | use the STRICLI Marks only in connection with goods and services of a level 51 | of quality at least equal to the goods and services it has distributed 52 | previously and, in no event, at no less than then-current industry standards; 53 | (ii) use the STRICLI Marks in strict compliance with all guidelines 54 | provided by Bloomberg and with such trademark notices as Bloomberg shall 55 | direct; (iii) upon reasonable request provide Bloomberg with specimens of use 56 | of the STRICLI Marks; and (iv) cease any use of the STRICLI Marks upon 57 | written notice from Bloomberg. 58 | 59 | - Agrees only to use the STRICLI logo marks provided in the file located in 60 | the 'docs' folder at https://github.com/bloomberg/STRICLI, to maintain 61 | aspect ratio and to refrain from modifying the color of the marks. 62 | 63 | - Agrees to use the word mark STRICLI only in the format “STRICLI”, 64 | “Stricli” or “stricli” in the same font and size as the surrounding text. 65 | 66 | - Agrees not to use the BLOOMBERG trademark, service mark or logo mark without 67 | permission and understands that any use of the BLOOMBERG trademark, service 68 | mark or logo mark may be made only following the grant of a license by 69 | Bloomberg. In referring to the STRICLI software, you may use the following 70 | text: “STRICLI, developed and published by Bloomberg L.P.” 71 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | packages/*/ 23 | !packages/sidebars.ts 24 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Local Development 6 | 7 | ``` 8 | $ nx start 9 | ``` 10 | 11 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 12 | 13 | ### Build 14 | 15 | ``` 16 | $ nx build 17 | ``` 18 | 19 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 20 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/blog/authors.yml: -------------------------------------------------------------------------------- 1 | mmolisani: 2 | name: Michael Molisani 3 | title: Project Maintainer 4 | image_url: https://github.com/molisani.png 5 | socials: 6 | github: molisani 7 | -------------------------------------------------------------------------------- /docs/docs/features/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Features", 3 | "position": 3, 4 | "collapsible": false, 5 | "link": { 6 | "type": "generated-index" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapsible": true 3 | } 4 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/array-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, ...paths: string[]) { 5 | this.process.stdout.write(`Deleting files at ${paths.join(", ")}`); 6 | }, 7 | parameters: { 8 | positional: { 9 | kind: "array", 10 | parameter: { 11 | brief: "File paths", 12 | parse: String, 13 | }, 14 | }, 15 | }, 16 | docs: { 17 | brief: "Example for live playground with homogenous positional parameters", 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/boolean-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | quiet: boolean; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { quiet }: Flags) { 9 | this.process.stdout.write(quiet ? "LOUD LOGGING" : "quiet logging"); 10 | }, 11 | parameters: { 12 | flags: { 13 | quiet: { 14 | kind: "boolean", 15 | brief: "Lowers logging level", 16 | }, 17 | }, 18 | }, 19 | docs: { 20 | brief: "Example for live playground with boolean flag", 21 | customUsage: [ 22 | { input: "--quiet", brief: "Flag with no value" }, 23 | { input: "--quiet=yes", brief: "Flag with explicit value" }, 24 | { input: "--noQuiet", brief: "Negated flag" }, 25 | ], 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/bounded-array-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, ...ids: number[]) { 5 | this.process.stdout.write(`Grouping users with IDs: ${ids.join(", ")}`); 6 | }, 7 | parameters: { 8 | positional: { 9 | kind: "array", 10 | parameter: { 11 | brief: "User ID numbers", 12 | parse: numberParser, 13 | }, 14 | minimum: 2, 15 | maximum: 4, 16 | }, 17 | }, 18 | docs: { 19 | brief: "Example for live playground with bounded positional parameters", 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/counter-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | verbose: number; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { verbose }: Flags) { 9 | this.process.stdout.write(`Logging with verbosity level ${verbose}`); 10 | }, 11 | parameters: { 12 | flags: { 13 | verbose: { 14 | kind: "counter", 15 | brief: "Controls how verbose logging should be", 16 | }, 17 | }, 18 | aliases: { 19 | v: "verbose" 20 | }, 21 | }, 22 | docs: { 23 | brief: "Example for live playground with counter flag", 24 | customUsage: [ 25 | "--verbose", 26 | "-v", 27 | "-vv", 28 | "-vv -v", 29 | ], 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/default-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | lineEnding: "lf" | "crlf"; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { lineEnding }: Flags) { 9 | this.process.stdout.write(`Switched line ending to ${lineEnding}`); 10 | }, 11 | parameters: { 12 | flags: { 13 | lineEnding: { 14 | kind: "enum", 15 | values: ["lf", "crlf"], 16 | brief: "Line ending characters", 17 | default: "lf", 18 | }, 19 | }, 20 | }, 21 | docs: { 22 | brief: "Example for live playground with flag configured with default value", 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/default-tuple-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, outputPath: string) { 5 | this.process.stdout.write(`Printing file to ${outputPath}`); 6 | }, 7 | parameters: { 8 | positional: { 9 | kind: "tuple", 10 | parameters: [ 11 | { 12 | brief: "File for intended output", 13 | parse: String, 14 | default: "output.txt" 15 | }, 16 | ], 17 | }, 18 | }, 19 | docs: { 20 | brief: "Example for live playground with positional parameter configured with default value", 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/enum-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | level: "info" | "warn" | "error"; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { level }: Flags) { 9 | this.process.stdout.write(`Set logging level to ${level}`); 10 | }, 11 | parameters: { 12 | flags: { 13 | level: { 14 | kind: "enum", 15 | values: ["info", "warn", "error"], 16 | brief: "Logging severity level", 17 | }, 18 | }, 19 | aliases: { 20 | l: "level", 21 | }, 22 | }, 23 | docs: { 24 | brief: "Example for live playground with enum flag" 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/hidden-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | visible?: boolean; 5 | hidden?: boolean; 6 | } 7 | 8 | export const root = buildCommand({ 9 | func(this: CommandContext, { visible, hidden }: Flags) { 10 | this.process.stdout.write(visible ? "Visible flag active" : "Visible flag inactive"); 11 | if (hidden) { 12 | this.process.stdout.write("Hidden flag active"); 13 | } 14 | }, 15 | parameters: { 16 | flags: { 17 | visible: { 18 | kind: "boolean", 19 | brief: "Visible flag", 20 | optional: true, 21 | }, 22 | hidden: { 23 | kind: "boolean", 24 | brief: "Hidden flag", 25 | optional: true, 26 | hidden: true, 27 | }, 28 | }, 29 | }, 30 | docs: { 31 | brief: "Example for live playground with hidden flag" 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/optional-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | limit?: number; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { limit }: Flags) { 9 | this.process.stdout.write(limit ? `Set limit to ${limit}` : "No limit"); 10 | }, 11 | parameters: { 12 | flags: { 13 | limit: { 14 | kind: "parsed", 15 | parse: numberParser, 16 | brief: "Upper limit on number of items", 17 | optional: true, 18 | }, 19 | }, 20 | }, 21 | docs: { 22 | brief: "Example for live playground with optional flag", 23 | customUsage: [ 24 | "", 25 | "--limit 1000", 26 | "--noQuiet", 27 | ], 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/optional-tuple-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, firstName: string, lastName?: string) { 5 | if (lastName) { 6 | this.process.stdout.write(`Hello ${firstName} ${lastName}!`); 7 | } else { 8 | this.process.stdout.write(`Hello ${firstName}!`); 9 | } 10 | }, 11 | parameters: { 12 | positional: { 13 | kind: "tuple", 14 | parameters: [ 15 | { 16 | brief: "First name", 17 | parse: String, 18 | }, 19 | { 20 | brief: "Last name", 21 | parse: String, 22 | optional: true, 23 | }, 24 | ], 25 | }, 26 | }, 27 | docs: { 28 | brief: "Example for live playground with optional positional parameter", 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/parsed-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | item: string; 5 | price: number; 6 | } 7 | 8 | export const root = buildCommand({ 9 | func(this: CommandContext, { item, price }: Flags) { 10 | this.process.stdout.write(`${item}s cost $${price.toFixed(2)}`); 11 | }, 12 | parameters: { 13 | flags: { 14 | item: { 15 | kind: "parsed", 16 | parse: String, // Effectively a no-op 17 | brief: "Item to display", 18 | }, 19 | price: { 20 | kind: "parsed", 21 | parse: numberParser, // Like Number() but throws on NaN 22 | brief: "Price of the item", 23 | }, 24 | }, 25 | }, 26 | docs: { 27 | brief: "Example for live playground with parsed flags", 28 | customUsage: [ 29 | "--item apple --price 1", 30 | "--item orange --price 3.5", 31 | "--item grape --price 6.25", 32 | ], 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/placeholder-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, src: string, dest: string) { 5 | this.process.stdout.write(`Copying file from ${src} to ${dest}`); 6 | }, 7 | parameters: { 8 | positional: { 9 | kind: "tuple", 10 | parameters: [ 11 | { 12 | brief: "Source file", 13 | parse: String, 14 | placeholder: "src", 15 | }, 16 | { 17 | brief: "Destination path", 18 | parse: String, 19 | placeholder: "dest", 20 | }, 21 | ], 22 | }, 23 | }, 24 | docs: { 25 | brief: "Example for live playground with positional arguments labeled by placeholders", 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/tuple-argument.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, type CommandContext } from "@stricli/core"; 2 | 3 | export const root = buildCommand({ 4 | func(this: CommandContext, _: {}, from: string, to: string) { 5 | this.process.stdout.write(`Moving file from ${from} to ${to}`); 6 | }, 7 | parameters: { 8 | positional: { 9 | kind: "tuple", 10 | parameters: [ 11 | { 12 | brief: "Origin path", 13 | parse: String, 14 | }, 15 | { 16 | brief: "Destination path", 17 | parse: String, 18 | }, 19 | ], 20 | }, 21 | }, 22 | docs: { 23 | brief: "Example for live playground with positional parameters", 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/examples/variadic-flag.txt: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type CommandContext } from "@stricli/core"; 2 | 3 | interface Flags { 4 | id: number[]; 5 | } 6 | 7 | export const root = buildCommand({ 8 | func(this: CommandContext, { id }: Flags) { 9 | this.process.stdout.write(`Selected following IDs: ${id.join(", ")}`); 10 | }, 11 | parameters: { 12 | flags: { 13 | id: { 14 | kind: "parsed", 15 | parse: numberParser, 16 | brief: "Set of IDs", 17 | variadic: true, 18 | }, 19 | }, 20 | aliases: { 21 | i: "id", 22 | }, 23 | }, 24 | docs: { 25 | brief: "Example for live playground with variadic flag", 26 | customUsage: [ 27 | "--id 10", 28 | "--id 10 --id 20 --id 30", 29 | "--id 5 -i 10 -i 15", 30 | ], 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /docs/docs/features/argument-parsing/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Argument Parsing 6 | 7 | Extending from our core principle of [When Parsing, Form Follows Function](../../getting-started/principles.mdx#when-parsing-form-follows-function), Stricli infers the shape of parameter definitions from the TypeScript types used in the implementation. This is achieved with some advanced conditional types that map the types of the parameters to the types of the parser specifications. 8 | 9 | Stricli supports both [named flags](./flags.mdx) and [positional arguments](./positional.mdx) when defining parameters. 10 | 11 | :::info[TypeScript `strict` recommended for type checking] 12 | 13 | Stricli relies on TypeScript to infer the types of the parameters. Through a series of conditional types, the type of the parameters in the implementation function are mapped to the types of the parser specifications. 14 | 15 | Many of these transforms do not behave as expected when the TypeScript compiler is not configured with `strict: true`. Specifically, the option `strictNullChecks` is known to be incompatible with the conditional types used to infer if a parameter is optional or not. 16 | 17 | So in order to ensure that the types are correctly inferred, it is **strongly recommended** that all Stricli projects (using TypeScript) are built with `strict: true`. 18 | 19 | ::: 20 | -------------------------------------------------------------------------------- /docs/docs/features/command-routing/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapsible": true 3 | } 4 | -------------------------------------------------------------------------------- /docs/docs/features/command-routing/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Command Routing 6 | 7 | Applications are the top-level object that encapsulate all of the logic for a given CLI application. They can be built with a single [command](./commands.mdx) as the root or a [route map](./route-maps.mdx) of nested subcommands. 8 | 9 | :::caution 10 | 11 | If [version information](../configuration.mdx#version-information) is provided for an application with a single command as the root, then that command must not use the `--version` flag or the `-v` flag alias as those are reserved. 12 | 13 | ::: 14 | 15 | When defining an application, there are several [configurations](../configuration.mdx) that should be specified (although all have default values). 16 | -------------------------------------------------------------------------------- /docs/docs/features/isolated-context.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Isolated Context 6 | 7 | At the simplest level, command line applications require few external dependencies. They need to be able to write to `stdout` for console output and `stderr` for errors. For Stricli, these requirements are encapsulated in the [`CommandContext`](../../packages/core/interfaces/CommandContext) type. 8 | 9 | It is a simple object that stores a `process` property that has `stdout`/`stderr` writable streams. This context is required when running the app, and is how Stricli prints help text and error messages to the console. 10 | 11 | For Node or Node-compatible applications, this is as simple as passing `{ process }` or `globalThis` to `run`. 12 | 13 | ## Application Context 14 | 15 | This object serves double duty as the context for the command and the application itself. There are some additional options that control the application's behavior. The first is `process.exit()` which allows Stricli to set the exit code of the application once the command finishes (or throws an error). The other is `locale` which is used by the [localization logic](configuration.mdx#localization) to determine which language the text should be in. 16 | 17 | ## Custom Data 18 | 19 | The provided context is bound to `this` on the command's implementation function. You can choose to ignore this context completely and log with `console.log` or `console.error`. However, the context type can be customized which opens up some more options via dependency injection. You can define a custom context to store arbitrary data, which will then get passed through to your command. 20 | 21 | ```ts 22 | // output-next-line 23 | /// types.ts 24 | interface User { 25 | readonly id: number; 26 | readonly name: string; 27 | } 28 | 29 | interface CustomContext extends CommandContext { 30 | readonly user?: User; 31 | } 32 | 33 | // output-next-line 34 | /// impl.ts 35 | export default function(this: CustomContext) { 36 | if (this.user) { 37 | this.process.stdout.write(`Logged in as ${this.user.name}`); 38 | } else { 39 | this.process.stdout.write(`Not logged in`); 40 | } 41 | } 42 | 43 | // output-next-line 44 | /// run.ts 45 | const user = ... // load user 46 | await run(app, process.argv.slice(2), { process, user }); 47 | ``` 48 | 49 | In this example, imagine that you store user information in the user's local environment. You can fetch that information and store it in the context for use in any/all of your commands. 50 | 51 | The _real_ benefit of this pattern is being able to fully test the implementation functions by controlling all of their inputs and dependencies. Check out the [testing section](../testing.mdx) for more information on how to test your commands. 52 | -------------------------------------------------------------------------------- /docs/docs/features/shell-autocomplete.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Shell Autocomplete 6 | 7 | Stricli has first class support for tab auto completion in shells. 8 | 9 | Rather than generate shell-specific scripts based on your application, the logic for proposing input completions is written directly into the core library. This means that the auto complete behavior will exactly match the functionality of your app. 10 | 11 | ## Autocomplete Management 12 | 13 | Part of the difficulty with setting up auto completion is getting it installed into the shell. Stricli solves this with a standalone command `npx @stricli/auto-complete@latest`. 14 | 15 | ``` 16 | USAGE 17 | @stricli/auto-complete install [--shell bash] targetCmd autcCmd 18 | @stricli/auto-complete uninstall [--shell bash] targetCmd 19 | @stricli/auto-complete --help 20 | @stricli/auto-complete --version 21 | 22 | Manage auto-complete command installations for shells 23 | 24 | FLAGS 25 | -h --help Print this help information and exit 26 | -v --version Print version information and exit 27 | 28 | COMMANDS 29 | install Installs target command with autocompletion for a given shell type 30 | uninstall Uninstalls target command for a given shell type 31 | ``` 32 | 33 | This command allows you to configure your shell to run a secondary `autcCmd` command whenever auto completion is requested for the `targetCmd` command. 34 | 35 | :::caution 36 | 37 | At this time, the only shell that `@stricli/auto-complete` supports is `bash`. Support for other shells is planned for the future. 38 | 39 | ::: 40 | 41 | ### Adding a Built-In `install` Command 42 | 43 | To simplify the process of installing the auto complete command, `@stricli/auto-complete` also exposes methods that allow you to build a command to be added to your own app. It is recommended to hide these so that they do not show up in any help text, but they can still be run. To see an example of this setup, check out the [quick start](../quick-start.mdx). 44 | -------------------------------------------------------------------------------- /docs/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting Started", 3 | "position": 1, 4 | "collapsible": true, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "Everything about what Stricli is and where it fits in the larger ecosystem." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/getting-started/faq.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Frequently Asked Questions 6 | 7 | ### Why is it called "stricli"? 8 | 9 | The name is a combination of "strict" and CLI. Strict refers to the strict input parsing as well as the strict TypeScript type checking. This word was also chosen because it looks/sounds somewhat like the English word "strictly". 10 | 11 | ### What is the logo? 12 | 13 | import StricliLogo from "../../static/img/S-logo.svg"; 14 | 15 | 16 | 17 | The current Stricli logo is a combination of s (for Stricli) and > to represent the terminal prompt. The specific s used in the logo is from the wonderful variable font Tourney which is licensed under the [OFL](https://github.com/Etcetera-Type-Co/Tourney/blob/master/OFL.txt). 18 | -------------------------------------------------------------------------------- /docs/docs/getting-started/overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Overview 6 | 7 | **Stricli** is a zero-dependency framework for building complex, highly-featured CLIs in TypeScript. 8 | 9 | Stricli was created despite an existing set of CLI frameworks in the JavaScript ecosystem. Bloomberg maintains several internal, user-facing CLIs written in TypeScript on top of Node.js using an assortment of existing frameworks and libraries. In the opinion of the authors of this framework, these existing frameworks and libraries suffer from some shortcomings and pain points that warranted an alternative solution. Check out the [alternatives considered](./alternatives) for a concrete breakdown. 10 | 11 | The common issues with these alternatives can be reframed as a set of language-agnostic [guiding principles](./principles) that should be considered in addition to [clig.dev](https://clig.dev) when analyzing a CLI framework. 12 | -------------------------------------------------------------------------------- /docs/docs/getting-started/principles.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Guiding Principles 6 | 7 | Much of this work was inspired by and built upon the incredible [Command Line Interface Guidelines](https://clig.dev). They are a set of open-source rules and recommendations for developing command line interfaces. This framework adheres to these guidelines, but it does not implement _all_ of them. Some features were considered [out-of-scope](../features/out-of-scope.mdx) for this framework, and are already solved by existing packages in the ecosystem. 8 | 9 | The following are principles that were found to be common traits of frameworks with good design. 10 | 11 | ## Commands Are Just Functions 12 | 13 | > **tl;dr** - Every command that is exposed by a CLI should just be a function underneath. 14 | 15 | Command line interfaces exist to provide functionality to users without needing to write code against a programmatic API. To put this another way, a CLI is merely a means for a user to invoke a certain function with specific arguments. These arguments are provided on the command line as text input and should translate directly to function arguments. 16 | 17 | It is rare that an advanced CLI application exposes only a single function, so the framework should be able to support a nested set of commands that are all reachable from certain routes. 18 | 19 | To support the natural syntax of functions, an application should simultaneously understand both named, unordered flags as well as positional arguments. Check out [clig.dev section on `Arguments and flags`](https://clig.dev/#arguments-and-flags) for specific guidelines on how these different input types should be formatted/interpreted. 20 | 21 | ## When Parsing, Form Follows Function 22 | 23 | > **tl;dr** - If function and command line arguments are linked, then function arguments should define command line parsing. 24 | 25 | Given a set of arguments for a function, the corresponding parser should type check against them completely. The arguments for the function are the source of truth, not the other way around. Invalid arguments should be caught by the framework before the command is executed. This includes missing arguments, extraneous arguments, and any arguments that are otherwise incorrectly formatted or structured. 26 | 27 | When inputs are parsed without an intended end state, it then falls to the individual functions to define which inputs are valid. The application logic for a CLI should not be directly responsible for validating user input, [within reason](../features/out-of-scope.mdx#cross-argument-validation). 28 | 29 | ## No "Magic" Features or Patterns 30 | 31 | > **tl;dr** - Developers should be able to understand and debug a framework using native tools for the language of that framework. 32 | 33 | While too much code can hinder readability, not enough code can completely kill it. "Don't Repeat Yourself" is an important principle in itself, but that does not always justify replacing code with custom conventions. When a framework has too much "magic" and relies on systems external to the code, it reduces portability and locks developers into certain patterns or tools. It is better to rely on the existing features of a language or environment, and if they don't exist that can be an opportunity to standardize rather than circumvent. 34 | -------------------------------------------------------------------------------- /docs/docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import ts from "typescript-eslint"; 2 | import common from "../eslint.config.mjs"; 3 | 4 | export default [ 5 | ...ts.configs.strictTypeChecked, 6 | ...common, 7 | { 8 | languageOptions: { 9 | parserOptions: { 10 | projectService: true, 11 | }, 12 | }, 13 | 14 | rules: { 15 | "@typescript-eslint/no-unnecessary-condition": "off", 16 | }, 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stricli/docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build-docs": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "format": "prettier --config ../.prettierrc -w .", 16 | "format:check": "prettier --config ../.prettierrc -c .", 17 | "lint": "eslint src" 18 | }, 19 | "dependencies": { 20 | "@docusaurus/core": "3.7.0", 21 | "@docusaurus/preset-classic": "3.7.0", 22 | "@docusaurus/theme-common": "^3.7.0", 23 | "@mdx-js/react": "^3.0.1", 24 | "@monaco-editor/loader": "^1.4.0", 25 | "@monaco-editor/react": "^4.6.0", 26 | "@typescript/sandbox": "^0.1.7", 27 | "anser": "^2.1.1", 28 | "clsx": "^2.0.0", 29 | "docusaurus-plugin-sass": "^0.2.5", 30 | "prism-react-renderer": "^2.3.0", 31 | "react": "^18.0.0", 32 | "react-dom": "^18.0.0", 33 | "typescript": "5.6.x" 34 | }, 35 | "devDependencies": { 36 | "@docusaurus/module-type-aliases": "3.7.0", 37 | "@docusaurus/tsconfig": "3.7.0", 38 | "@docusaurus/types": "3.7.0", 39 | "@stricli/core": "^1.0.0", 40 | "docusaurus-plugin-typedoc": "^1.0.5", 41 | "monaco-editor": "^0.51.0", 42 | "sass": "^1.79.4", 43 | "typedoc": "^0.26.7", 44 | "typedoc-plugin-markdown": "^4.2.8" 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.5%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 3 chrome version", 54 | "last 3 firefox version", 55 | "last 5 safari version" 56 | ] 57 | }, 58 | "engines": { 59 | "node": ">=18.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/packages/index.mdx: -------------------------------------------------------------------------------- 1 | # Packages 2 | 3 | - [`@stricli/core`](./core/index.md) 4 | - [`@stricli/auto-complete`](./auto-complete/index.md) 5 | -------------------------------------------------------------------------------- /docs/packages/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | typedocSidebar: [ 15 | { 16 | type: "category", 17 | label: "Packages", 18 | link: { 19 | type: "doc", 20 | id: "index", 21 | }, 22 | items: [ 23 | { 24 | type: "category", 25 | label: "@stricli/core", 26 | collapsed: true, 27 | link: { 28 | type: "doc", 29 | id: "core/index", 30 | }, 31 | items: require("./core/typedoc-sidebar.cjs"), 32 | }, 33 | { 34 | type: "category", 35 | label: "@stricli/auto-complete", 36 | collapsed: true, 37 | link: { 38 | type: "doc", 39 | id: "auto-complete/index", 40 | }, 41 | items: require("./auto-complete/typedoc-sidebar.cjs"), 42 | }, 43 | ], 44 | }, 45 | ], 46 | 47 | // But you can create a sidebar manually 48 | /* 49 | tutorialSidebar: [ 50 | 'intro', 51 | 'hello', 52 | { 53 | type: 'category', 54 | label: 'Tutorial', 55 | items: ['tutorial-basics/create-a-document'], 56 | }, 57 | ], 58 | */ 59 | }; 60 | 61 | export default sidebars; 62 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import React from "react"; 4 | import clsx from "clsx"; 5 | import styles from "./styles.module.css"; 6 | import Link from "@docusaurus/Link"; 7 | 8 | type FeatureItem = { 9 | title: string; 10 | Svg?: React.ComponentType>; 11 | description: React.JSX.Element; 12 | }; 13 | 14 | const FeatureList: FeatureItem[] = [ 15 | { 16 | title: "Full TypeScript Support", 17 | description: ( 18 | <> 19 | TypeScript types for command named flags and{" "} 20 | positional arguments are defined once and 21 | then flow through the entire application. 22 | 23 | ), 24 | }, 25 | { 26 | title: "Zero Dependencies", 27 | description: ( 28 | <> 29 | Stricli is a self-contained command line parser that has no runtime dependencies. This is due to the 30 | powerful, yet strictly limited scope of supported features. 31 | 32 | ), 33 | }, 34 | { 35 | title: "Dual ESM + CommonJS", 36 | description: ( 37 | <>This package is published with support for both ESM and CJS consumers (although ESM is recommended). 38 | ), 39 | }, 40 | { 41 | title: "Ready for Code Splitting", 42 | description: ( 43 | <> 44 | Command implementations are defined separately from their parameters, allowing for async imports and 45 | code splitting with ESM build tools. Run --help without ever importing a single runtime 46 | dependency. 47 | 48 | ), 49 | }, 50 | { 51 | title: "Optional Dependency Injection", 52 | description: ( 53 | <> 54 | All system access is encapsulated in a single{" "} 55 | context object which allows for easier dependency 56 | injection and mocking for unit tests. 57 | 58 | ), 59 | }, 60 | { 61 | title: "Dynamic Autocomplete", 62 | description: ( 63 | <> 64 | Stricli has first class support for{" "} 65 | shell autocomplete that can include custom dynamic 66 | suggestions at runtime. 67 | 68 | ), 69 | }, 70 | ]; 71 | 72 | function Feature({ title, Svg, description }: FeatureItem) { 73 | return ( 74 |
75 | {Svg && ( 76 |
77 | 78 |
79 | )} 80 |
81 |

{title}

82 |

{description}

83 |
84 |
85 | ); 86 | } 87 | 88 | export default function HomepageFeatures(): React.JSX.Element { 89 | return ( 90 |
91 |
92 |
93 | {FeatureList.map((props, idx) => ( 94 | 95 | ))} 96 |
97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/StricliPlayground/Editor/MonacoContainer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | // Adapted from monaco-react/src/MonacoContainer/MonacoContainer.tsx 5 | import React, { CSSProperties, ReactNode, RefObject } from "react"; 6 | 7 | const styles: Record = { 8 | wrapper: { 9 | display: "flex", 10 | position: "relative", 11 | textAlign: "initial", 12 | }, 13 | fullWidth: { 14 | width: "100%", 15 | }, 16 | hide: { 17 | display: "none", 18 | }, 19 | }; 20 | 21 | // import Loading from '../Loading'; 22 | export type ContainerProps = { 23 | width: number | string; 24 | height: number | string; 25 | isEditorReady: boolean; 26 | loading: ReactNode | string; 27 | _ref: RefObject; 28 | className?: string; 29 | wrapperProps?: object; 30 | }; 31 | 32 | // ** forwardref render functions do not support proptypes or defaultprops ** 33 | // one of the reasons why we use a separate prop for passing ref instead of using forwardref 34 | 35 | export default function MonacoContainer({ 36 | width, 37 | height, 38 | isEditorReady, 39 | loading, 40 | _ref, 41 | className, 42 | wrapperProps, 43 | }: ContainerProps) { 44 | return ( 45 |
49 | {!isEditorReady &&
{loading}
} 50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /docs/src/components/StricliPlayground/Terminal/Ansi.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { riffle } from "@site/src/util/array"; 4 | import { ansiToJson } from "anser"; 5 | import React from "react"; 6 | 7 | export interface AnsiProps { 8 | children?: string; 9 | className?: string; 10 | } 11 | 12 | export default function Ansi({ children = "", className = "" }: AnsiProps) { 13 | const json = ansiToJson(children, { use_classes: true }); 14 | const classes = ["ansi-block"]; 15 | if (className) { 16 | classes.push(className); 17 | } 18 | return ( 19 |
20 | {json.flatMap((entry, i) => { 21 | const classes: string[] = []; 22 | if (entry.fg) { 23 | classes.push(entry.fg); 24 | } 25 | if (entry.bg) { 26 | classes.push(entry.bg); 27 | } 28 | if (entry.decorations) { 29 | classes.push(...entry.decorations.map((decoration) => `ansi-${decoration}`)); 30 | } 31 | const lines = entry.content.split("\n").map((part, j) => { 32 | const className = classes.join(" ") || void 0; 33 | return ( 34 | 35 | {part} 36 | 37 | ); 38 | }); 39 | return riffle(lines, (k) =>
); 40 | })} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /docs/src/components/StricliPlayground/hooks.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { useEffect, useRef, EffectCallback, DependencyList } from "react"; 4 | 5 | export function useMount(effect: EffectCallback) { 6 | useEffect(effect, []); 7 | } 8 | 9 | export function usePrevious(value: T) { 10 | const ref = useRef(); 11 | 12 | useEffect(() => { 13 | ref.current = value; 14 | }, [value]); 15 | 16 | return ref.current; 17 | } 18 | 19 | export function useUpdate(effect: EffectCallback, deps: DependencyList, applyChanges = true) { 20 | const isInitialMount = useRef(true); 21 | 22 | useEffect( 23 | isInitialMount.current || !applyChanges 24 | ? () => { 25 | isInitialMount.current = false; 26 | } 27 | : effect, 28 | deps, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/components/StricliPlayground/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import BrowserOnly from "@docusaurus/BrowserOnly"; 4 | import type { StricliPlaygroundProps } from "./impl"; 5 | 6 | export default function StricliPlayground(props: StricliPlaygroundProps): React.JSX.Element { 7 | return ( 8 | 9 | {() => { 10 | // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-unsafe-assignment 11 | const { default: Playground } = require("./impl") as import("./impl"); 12 | return ; 13 | }} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/components/StricliPlayground/styles.module.css: -------------------------------------------------------------------------------- 1 | .playgroundContainer { 2 | margin-bottom: var(--ifm-leading); 3 | border-radius: var(--ifm-global-radius); 4 | box-shadow: var(--ifm-global-shadow-lw); 5 | overflow: hidden; 6 | } 7 | 8 | .playgroundHeader { 9 | letter-spacing: 0.08rem; 10 | padding: 0.75rem; 11 | text-transform: uppercase; 12 | font-weight: bold; 13 | background: var(--ifm-color-emphasis-200); 14 | color: var(--ifm-color-content); 15 | font-size: var(--ifm-code-font-size); 16 | } 17 | 18 | .playgroundHeader:first-of-type { 19 | background: var(--ifm-color-emphasis-600); 20 | color: var(--ifm-color-content-inverse); 21 | } 22 | 23 | .playgroundEditor { 24 | font: var(--ifm-code-font-size) / var(--ifm-pre-line-height) var(--ifm-font-family-monospace) !important; 25 | /* rtl:ignore */ 26 | direction: ltr; 27 | } 28 | 29 | .playgroundPreview { 30 | padding: 1rem; 31 | background-color: var(--ifm-pre-background); 32 | } 33 | 34 | .errorFallback { 35 | padding: 0.55rem; 36 | } 37 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: left; 9 | position: relative; 10 | overflow: hidden; 11 | background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='30 30 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ebedf0' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); 12 | } 13 | 14 | @media screen and (max-width: 996px) { 15 | .heroBanner { 16 | padding: 2rem; 17 | } 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | align-items: left; 23 | justify-content: left; 24 | margin-bottom: 1em; 25 | } 26 | 27 | .buttons > a:not(:first-child) { 28 | margin-left: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import clsx from "clsx"; 4 | import Link from "@docusaurus/Link"; 5 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 6 | import Layout from "@theme/Layout"; 7 | import HomepageFeatures from "@site/src/components/HomepageFeatures"; 8 | import Heading from "@theme/Heading"; 9 | 10 | import styles from "./index.module.css"; 11 | 12 | function HomepageHeader() { 13 | const { siteConfig } = useDocusaurusContext(); 14 | return ( 15 |
16 |
17 | 18 | {siteConfig.title.toLowerCase()} 19 | 20 |

{siteConfig.tagline}

21 |
22 | 23 | Overview 24 | 25 | 26 | Quick Start 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export default function Home(): React.JSX.Element { 35 | const { siteConfig } = useDocusaurusContext(); 36 | return ( 37 | 38 | 39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/src/util/array.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | export function riffle(arr: T[], newItem: (index: number) => T): readonly T[] { 5 | return arr.reduce((result, item, i) => { 6 | result.push(item); 7 | if (i < arr.length - 1) { 8 | result.push(newItem(i)); 9 | } 10 | return result; 11 | }, []); 12 | } 13 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/stricli/beb8584382948ab8898e973d6a13dbbc21d274cb/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/S-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloomberg/stricli/beb8584382948ab8898e973d6a13dbbc21d274cb/docs/static/img/S-logo.ico -------------------------------------------------------------------------------- /docs/static/img/S-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 41 | 48 | 49 | 52 | 55 | 60 | 65 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "esModuleInterop": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat"; 2 | import js from "@eslint/js"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import header from "eslint-plugin-header"; 5 | import _import from "eslint-plugin-import"; 6 | import ts from "typescript-eslint"; 7 | 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.strict, 11 | { 12 | plugins: { 13 | header, 14 | import: fixupPluginRules(_import), 15 | }, 16 | 17 | linterOptions: { 18 | reportUnusedDisableDirectives: true, 19 | }, 20 | 21 | languageOptions: { 22 | parser: tsParser, 23 | }, 24 | 25 | rules: { 26 | "header/header": [ 27 | "error", 28 | "line", 29 | " Copyright 2024 Bloomberg Finance L.P.\n Distributed under the terms of the Apache 2.0 license.", 30 | 0, 31 | ], 32 | 33 | "no-restricted-imports": [ 34 | "error", 35 | { 36 | patterns: [ 37 | { 38 | group: ["**/index"], 39 | message: "Internal library code should never import from library root.", 40 | }, 41 | ], 42 | }, 43 | ], 44 | 45 | "import/no-extraneous-dependencies": [ 46 | "error", 47 | { 48 | devDependencies: false, 49 | }, 50 | ], 51 | 52 | "@typescript-eslint/no-import-type-side-effects": "error", 53 | 54 | "@typescript-eslint/restrict-template-expressions": [ 55 | "error", 56 | { 57 | allowNumber: true, 58 | }, 59 | ], 60 | 61 | "@typescript-eslint/consistent-type-definitions": "off", 62 | "@typescript-eslint/no-invalid-void-type": "off", 63 | "@typescript-eslint/require-await": "off", 64 | }, 65 | }, 66 | { 67 | files: ["tests/**/*.ts"], 68 | 69 | languageOptions: { 70 | parserOptions: { 71 | parserService: true, 72 | }, 73 | }, 74 | 75 | rules: { 76 | "no-restricted-imports": [ 77 | "error", 78 | { 79 | patterns: [ 80 | { 81 | group: ["**/src/*"], 82 | message: 83 | "Tests should always import from library root, except when testing internal-only code.", 84 | }, 85 | ], 86 | }, 87 | ], 88 | 89 | "import/no-extraneous-dependencies": "off", 90 | "@typescript-eslint/no-empty-function": "off", 91 | "@typescript-eslint/no-empty-object-type": "off", 92 | "@typescript-eslint/no-explicit-any": "off", 93 | "@typescript-eslint/no-non-null-assertion": "off", 94 | "@typescript-eslint/no-unsafe-argument": "off", 95 | "@typescript-eslint/no-unused-expressions": "off", 96 | "@typescript-eslint/no-unused-vars": "off", 97 | }, 98 | }, 99 | ]; 100 | -------------------------------------------------------------------------------- /examples/bun/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /examples/bun/README.md: -------------------------------------------------------------------------------- 1 | # stricli-bun-example 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run ./src/bin/cli.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.22. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /examples/bun/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stricli-bun-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "module": "src/app.ts", 6 | "type": "module", 7 | "devDependencies": { 8 | "@types/bun": "latest" 9 | }, 10 | "peerDependencies": { 11 | "typescript": "5.6.x" 12 | }, 13 | "dependencies": { 14 | "@stricli/core": "^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/bun/src/app.ts: -------------------------------------------------------------------------------- 1 | import { buildApplication, buildRouteMap } from "@stricli/core"; 2 | import { command as echo } from "./commands/echo.ts"; 3 | import { buildUnaryMathCommand, buildBinaryMathCommand, buildVariadicMathCommand } from "./commands/math.ts"; 4 | 5 | const math = buildRouteMap({ 6 | routes: { 7 | log: buildUnaryMathCommand("log"), 8 | sqrt: buildUnaryMathCommand("sqrt"), 9 | pow: buildBinaryMathCommand("pow"), 10 | max: buildVariadicMathCommand("max"), 11 | min: buildVariadicMathCommand("min"), 12 | }, 13 | docs: { 14 | brief: "Various math operations", 15 | }, 16 | }); 17 | 18 | const root = buildRouteMap({ 19 | routes: { 20 | echo, 21 | math, 22 | }, 23 | docs: { 24 | brief: "All available example commands", 25 | }, 26 | }); 27 | 28 | export const app = buildApplication(root, { 29 | name: "stricli-bun-example", 30 | }); 31 | -------------------------------------------------------------------------------- /examples/bun/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { run } from "@stricli/core"; 3 | import { app } from "../app.ts"; 4 | await run(app, process.argv.slice(2), { process }); 5 | -------------------------------------------------------------------------------- /examples/bun/src/commands/echo.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser } from "@stricli/core"; 2 | 3 | type Flags = { 4 | readonly count?: number; 5 | }; 6 | 7 | export const command = buildCommand({ 8 | func: (flags: Flags, text: string) => { 9 | const count = flags.count ?? 1; 10 | for (let i = 0; i < count; ++i) { 11 | console.log(text); 12 | } 13 | }, 14 | parameters: { 15 | flags: { 16 | count: { 17 | brief: "Number of times to repeat the argument", 18 | kind: "parsed", 19 | parse: numberParser, 20 | optional: true, 21 | }, 22 | }, 23 | positional: { 24 | kind: "tuple", 25 | parameters: [ 26 | { 27 | brief: "Text to repeat", 28 | parse: String, 29 | placeholder: "text", 30 | }, 31 | ], 32 | }, 33 | }, 34 | docs: { 35 | brief: "Echo the first argument to the console", 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/bun/src/commands/math.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type Command, type CommandContext } from "@stricli/core"; 2 | 3 | type MathFunction = keyof { 4 | [F in keyof Math as Math[F] extends (...values: number[]) => number ? F : never]: F; 5 | }; 6 | 7 | type MathFunctionArity = { 8 | [F in MathFunction as Parameters["length"]]: F; 9 | }; 10 | 11 | export function buildUnaryMathCommand(op: MathFunctionArity[1]): Command { 12 | return buildCommand({ 13 | func: (flags: {}, input: number) => { 14 | console.log(Math[op](input)); 15 | }, 16 | parameters: { 17 | positional: { 18 | kind: "tuple", 19 | parameters: [ 20 | { 21 | brief: "Input number", 22 | parse: numberParser, 23 | }, 24 | ], 25 | }, 26 | }, 27 | docs: { 28 | brief: `Run ${op} on input`, 29 | }, 30 | }); 31 | } 32 | 33 | export function buildBinaryMathCommand(op: MathFunctionArity[2]): Command { 34 | return buildCommand({ 35 | func: (flags: {}, input1: number, input2: number) => { 36 | console.log(Math[op](input1, input2)); 37 | }, 38 | parameters: { 39 | positional: { 40 | kind: "tuple", 41 | parameters: [ 42 | { 43 | brief: "First input number", 44 | parse: numberParser, 45 | }, 46 | { 47 | brief: "Second input number", 48 | parse: numberParser, 49 | }, 50 | ], 51 | }, 52 | }, 53 | docs: { 54 | brief: `Run ${op} on inputs`, 55 | }, 56 | }); 57 | } 58 | 59 | export function buildVariadicMathCommand(op: MathFunctionArity[number]): Command { 60 | return buildCommand({ 61 | func: (flags: {}, ...inputs: number[]) => { 62 | console.log(Math[op](...inputs)); 63 | }, 64 | parameters: { 65 | positional: { 66 | kind: "array", 67 | parameter: { 68 | brief: "List of input numbers", 69 | parse: numberParser, 70 | }, 71 | }, 72 | }, 73 | docs: { 74 | brief: `Run ${op} on inputs`, 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /examples/bun/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/deno/README.md: -------------------------------------------------------------------------------- 1 | # stricli-deno-example 2 | 3 | > NOTE: This example is intended to work with [Deno v2](https://deno.com/blog/v2.0-release-candidate) 4 | 5 | To run: 6 | 7 | ```bash 8 | deno run --allow-env src/bin/cli.ts 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/deno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stricli-deno-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "module": "src/app.ts", 6 | "type": "module", 7 | "devDependencies": { 8 | "@types/bun": "latest" 9 | }, 10 | "peerDependencies": { 11 | "typescript": "5.6.x" 12 | }, 13 | "dependencies": { 14 | "@stricli/core": "^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/deno/src/app.ts: -------------------------------------------------------------------------------- 1 | import { buildApplication, buildRouteMap } from "npm:@stricli/core"; 2 | import { command as echo } from "./commands/echo.ts"; 3 | import { buildUnaryMathCommand, buildBinaryMathCommand, buildVariadicMathCommand } from "./commands/math.ts"; 4 | 5 | const math = buildRouteMap({ 6 | routes: { 7 | log: buildUnaryMathCommand("log"), 8 | sqrt: buildUnaryMathCommand("sqrt"), 9 | pow: buildBinaryMathCommand("pow"), 10 | max: buildVariadicMathCommand("max"), 11 | min: buildVariadicMathCommand("min"), 12 | }, 13 | docs: { 14 | brief: "Various math operations", 15 | }, 16 | }); 17 | 18 | const root = buildRouteMap({ 19 | routes: { 20 | echo, 21 | math, 22 | }, 23 | docs: { 24 | brief: "All available example commands", 25 | }, 26 | }); 27 | 28 | export const app = buildApplication(root, { 29 | name: "stricli-deno-example", 30 | }); 31 | -------------------------------------------------------------------------------- /examples/deno/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { run } from "npm:@stricli/core@1.0.0"; 3 | import { app } from "../app.ts"; 4 | await run(app, process.argv.slice(2), { process }); 5 | -------------------------------------------------------------------------------- /examples/deno/src/commands/echo.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser } from "npm:@stricli/core"; 2 | 3 | type Flags = { 4 | readonly count?: number; 5 | }; 6 | 7 | export const command = buildCommand({ 8 | func: (flags: Flags, text: string) => { 9 | const count = flags.count ?? 1; 10 | for (let i = 0; i < count; ++i) { 11 | console.log(text); 12 | } 13 | }, 14 | parameters: { 15 | flags: { 16 | count: { 17 | brief: "Number of times to repeat the argument", 18 | kind: "parsed", 19 | parse: numberParser, 20 | optional: true, 21 | }, 22 | }, 23 | positional: { 24 | kind: "tuple", 25 | parameters: [ 26 | { 27 | brief: "Text to repeat", 28 | parse: String, 29 | placeholder: "text", 30 | }, 31 | ], 32 | }, 33 | }, 34 | docs: { 35 | brief: "Echo the first argument to the console", 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/deno/src/commands/math.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type Command, type CommandContext } from "npm:@stricli/core"; 2 | 3 | type MathFunction = keyof { 4 | [F in keyof Math as Math[F] extends (...values: number[]) => number ? F : never]: F; 5 | }; 6 | 7 | type MathFunctionArity = { 8 | [F in MathFunction as Parameters["length"]]: F; 9 | }; 10 | 11 | export function buildUnaryMathCommand(op: MathFunctionArity[1]): Command { 12 | return buildCommand({ 13 | func: (flags: {}, input: number) => { 14 | console.log(Math[op](input)); 15 | }, 16 | parameters: { 17 | positional: { 18 | kind: "tuple", 19 | parameters: [ 20 | { 21 | brief: "Input number", 22 | parse: numberParser, 23 | }, 24 | ], 25 | }, 26 | }, 27 | docs: { 28 | brief: `Run ${op} on input`, 29 | }, 30 | }); 31 | } 32 | 33 | export function buildBinaryMathCommand(op: MathFunctionArity[2]): Command { 34 | return buildCommand({ 35 | func: (flags: {}, input1: number, input2: number) => { 36 | console.log(Math[op](input1, input2)); 37 | }, 38 | parameters: { 39 | positional: { 40 | kind: "tuple", 41 | parameters: [ 42 | { 43 | brief: "First input number", 44 | parse: numberParser, 45 | }, 46 | { 47 | brief: "Second input number", 48 | parse: numberParser, 49 | }, 50 | ], 51 | }, 52 | }, 53 | docs: { 54 | brief: `Run ${op} on inputs`, 55 | }, 56 | }); 57 | } 58 | 59 | export function buildVariadicMathCommand(op: MathFunctionArity[number]): Command { 60 | return buildCommand({ 61 | func: (flags: {}, ...inputs: number[]) => { 62 | console.log(Math[op](...inputs)); 63 | }, 64 | parameters: { 65 | positional: { 66 | kind: "array", 67 | parameter: { 68 | brief: "List of input numbers", 69 | parse: numberParser, 70 | }, 71 | }, 72 | }, 73 | docs: { 74 | brief: `Run ${op} on inputs`, 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /examples/node/README.md: -------------------------------------------------------------------------------- 1 | # stricli-node-example 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | npm install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | tsx src/bin/cli.ts 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stricli-node-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "index.js", 7 | "dependencies": { 8 | "@stricli/core": "^1.0.0" 9 | }, 10 | "devDependencies": { 11 | "@types/node": "^22.1.0", 12 | "tsx": "^4.16.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/node/src/app.ts: -------------------------------------------------------------------------------- 1 | import { buildApplication, buildRouteMap } from "@stricli/core"; 2 | import { command as echo } from "./commands/echo.ts"; 3 | import { buildUnaryMathCommand, buildBinaryMathCommand, buildVariadicMathCommand } from "./commands/math.ts"; 4 | 5 | const math = buildRouteMap({ 6 | routes: { 7 | log: buildUnaryMathCommand("log"), 8 | sqrt: buildUnaryMathCommand("sqrt"), 9 | pow: buildBinaryMathCommand("pow"), 10 | max: buildVariadicMathCommand("max"), 11 | min: buildVariadicMathCommand("min"), 12 | }, 13 | docs: { 14 | brief: "Various math operations", 15 | }, 16 | }); 17 | 18 | const root = buildRouteMap({ 19 | routes: { 20 | echo, 21 | math, 22 | }, 23 | docs: { 24 | brief: "All available example commands", 25 | }, 26 | }); 27 | 28 | export const app = buildApplication(root, { 29 | name: "stricli-node-example", 30 | }); 31 | -------------------------------------------------------------------------------- /examples/node/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { run } from "@stricli/core"; 3 | import { app } from "../app.ts"; 4 | await run(app, process.argv.slice(2), { process }); 5 | -------------------------------------------------------------------------------- /examples/node/src/commands/echo.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser } from "@stricli/core"; 2 | 3 | type Flags = { 4 | readonly count?: number; 5 | }; 6 | 7 | export const command = buildCommand({ 8 | func: (flags: Flags, text: string) => { 9 | const count = flags.count ?? 1; 10 | for (let i = 0; i < count; ++i) { 11 | console.log(text); 12 | } 13 | }, 14 | parameters: { 15 | flags: { 16 | count: { 17 | brief: "Number of times to repeat the argument", 18 | kind: "parsed", 19 | parse: numberParser, 20 | optional: true, 21 | }, 22 | }, 23 | positional: { 24 | kind: "tuple", 25 | parameters: [ 26 | { 27 | brief: "Text to repeat", 28 | parse: String, 29 | placeholder: "text", 30 | }, 31 | ], 32 | }, 33 | }, 34 | docs: { 35 | brief: "Echo the first argument to the console", 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/node/src/commands/math.ts: -------------------------------------------------------------------------------- 1 | import { buildCommand, numberParser, type Command, type CommandContext } from "@stricli/core"; 2 | 3 | type MathFunction = keyof { 4 | [F in keyof Math as Math[F] extends (...values: number[]) => number ? F : never]: F; 5 | }; 6 | 7 | type MathFunctionArity = { 8 | [F in MathFunction as Parameters["length"]]: F; 9 | }; 10 | 11 | export function buildUnaryMathCommand(op: MathFunctionArity[1]): Command { 12 | return buildCommand({ 13 | func: (flags: {}, input: number) => { 14 | console.log(Math[op](input)); 15 | }, 16 | parameters: { 17 | positional: { 18 | kind: "tuple", 19 | parameters: [ 20 | { 21 | brief: "Input number", 22 | parse: numberParser, 23 | }, 24 | ], 25 | }, 26 | }, 27 | docs: { 28 | brief: `Run ${op} on input`, 29 | }, 30 | }); 31 | } 32 | 33 | export function buildBinaryMathCommand(op: MathFunctionArity[2]): Command { 34 | return buildCommand({ 35 | func: (flags: {}, input1: number, input2: number) => { 36 | console.log(Math[op](input1, input2)); 37 | }, 38 | parameters: { 39 | positional: { 40 | kind: "tuple", 41 | parameters: [ 42 | { 43 | brief: "First input number", 44 | parse: numberParser, 45 | }, 46 | { 47 | brief: "Second input number", 48 | parse: numberParser, 49 | }, 50 | ], 51 | }, 52 | }, 53 | docs: { 54 | brief: `Run ${op} on inputs`, 55 | }, 56 | }); 57 | } 58 | 59 | export function buildVariadicMathCommand(op: MathFunctionArity[number]): Command { 60 | return buildCommand({ 61 | func: (flags: {}, ...inputs: number[]) => { 62 | console.log(Math[op](...inputs)); 63 | }, 64 | parameters: { 65 | positional: { 66 | kind: "array", 67 | parameter: { 68 | brief: "List of input numbers", 69 | parse: numberParser, 70 | }, 71 | }, 72 | }, 73 | docs: { 74 | brief: `Run ${op} on inputs`, 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /examples/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "nodenext", 5 | "allowImportingTsExtensions": true, 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "targetDefaults": { 4 | "typecheck": { 5 | "dependsOn": ["^build"], 6 | "cache": true 7 | }, 8 | "build": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["{projectRoot}/dist"], 11 | "cache": true 12 | }, 13 | "format": { 14 | "inputs": ["{workspaceRoot}/.prettierrc"], 15 | "cache": false 16 | }, 17 | "format:check": { 18 | "inputs": ["{workspaceRoot}/.prettierrc"], 19 | "cache": true 20 | }, 21 | "lint": { 22 | "dependsOn": ["^build", "typecheck"], 23 | "inputs": ["{workspaceRoot}/eslint.config.mjs"], 24 | "cache": true 25 | }, 26 | "lint:fix": { 27 | "dependsOn": ["^build", "typecheck"], 28 | "inputs": ["{workspaceRoot}/eslint.config.mjs"], 29 | "cache": false 30 | }, 31 | "test": { 32 | "dependsOn": ["^build", "typecheck"], 33 | "cache": true 34 | }, 35 | "coverage": { 36 | "dependsOn": ["^build", "typecheck"], 37 | "outputs": ["{projectRoot}/coverage", "{projectRoot}/.nyc_output"], 38 | "cache": true 39 | }, 40 | "build-docs": { 41 | "dependsOn": ["^build"], 42 | "outputs": ["{projectRoot}/build", "{projectRoot}/.docusaurus"], 43 | "cache": true 44 | } 45 | }, 46 | "defaultBase": "main", 47 | "release": { 48 | "projects": ["@stricli/*", "!@stricli/docs"], 49 | "releaseTagPattern": "v{version}", 50 | "version": { 51 | "conventionalCommits": true 52 | }, 53 | "changelog": { 54 | "workspaceChangelog": { 55 | "createRelease": "github" 56 | } 57 | }, 58 | "git": { 59 | "commitMessage": "chore(release): {version}", 60 | "commitArgs": "--signoff" 61 | } 62 | }, 63 | "namedInputs": { 64 | "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stricli", 3 | "private": true, 4 | "license": "Apache-2.0", 5 | "workspaces": [ 6 | "docs", 7 | "packages/*", 8 | "examples/*" 9 | ], 10 | "packageManager": "npm@10.4.0", 11 | "nx": {}, 12 | "scripts": { 13 | "format": "prettier --config .prettierrc -w *.md *.json *.mjs", 14 | "format:check": "prettier --config .prettierrc -c *.md *.json *.mjs" 15 | }, 16 | "devDependencies": { 17 | "@eslint/compat": "^1.1.1", 18 | "@eslint/eslintrc": "^3.1.0", 19 | "@eslint/js": "^9.10.0", 20 | "@nx/eslint": "20.6.2", 21 | "@types/eslint__js": "^8.42.3", 22 | "@typescript-eslint/eslint-plugin": "^8.2.0", 23 | "eslint": "~8.57.0", 24 | "eslint-plugin-header": "^3.1.1", 25 | "nx": "20.6.2", 26 | "prettier": "^3.3.3", 27 | "typescript": "^5.6.2", 28 | "typescript-eslint": "^8.6.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/auto-complete/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # TypeScript 107 | lib/ 108 | 109 | # esbuild 110 | build/ 111 | -------------------------------------------------------------------------------- /packages/auto-complete/README.md: -------------------------------------------------------------------------------- 1 | # `@stricli/auto-complete` 2 | 3 | > Common utilities for enhancing Stricli applications with autocomplete 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/@stricli/auto-complete.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/auto-complete) 6 | [![NPM Type Definitions](https://img.shields.io/npm/types/@stricli/auto-complete.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/auto-complete) 7 | [![NPM Downloads](https://img.shields.io/npm/dm/@stricli/auto-complete.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/auto-complete) 8 | 9 | 👉 See **https://bloomberg.github.io/stricli** for documentation on Stricli and **https://bloomberg.github.io/stricli/packages/auto-complete** for the API docs of this package. 10 | -------------------------------------------------------------------------------- /packages/auto-complete/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import common from "../../eslint.config.mjs"; 2 | import ts from "typescript-eslint"; 3 | 4 | export default [ 5 | ...ts.configs.strictTypeChecked, 6 | ...common, 7 | { 8 | languageOptions: { 9 | parserOptions: { 10 | project: ["src/tsconfig.json"], 11 | }, 12 | }, 13 | }, 14 | { 15 | files: ["tests/**/*.ts"], 16 | languageOptions: { 17 | parserOptions: { 18 | project: ["tsconfig.json"], 19 | }, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /packages/auto-complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stricli/auto-complete", 3 | "version": "1.1.2", 4 | "description": "Common utilities for enhancing Stricli applications with autocomplete", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bloomberg/stricli/tree/main/packages/auto-complete" 9 | }, 10 | "author": "Michael Molisani ", 11 | "files": [ 12 | "dist" 13 | ], 14 | "type": "module", 15 | "exports": { 16 | "import": "./dist/index.js", 17 | "require": "./dist/index.cjs" 18 | }, 19 | "types": "dist/index.d.ts", 20 | "bin": "dist/bin/cli.js", 21 | "scripts": { 22 | "format": "prettier --config ../../.prettierrc -w .", 23 | "format:check": "prettier --config ../../.prettierrc -c .", 24 | "lint": "eslint src", 25 | "lint:fix": "eslint src --fix", 26 | "typecheck": "tsc -p tsconfig.json --noEmit", 27 | "build": "tsup", 28 | "prepublishOnly": "npm run build" 29 | }, 30 | "tsup": { 31 | "entry": [ 32 | "src/index.ts", 33 | "src/bin/cli.ts" 34 | ], 35 | "format": [ 36 | "esm", 37 | "cjs" 38 | ], 39 | "tsconfig": "src/tsconfig.json", 40 | "dts": true, 41 | "minify": true, 42 | "clean": true 43 | }, 44 | "devDependencies": { 45 | "@typescript-eslint/eslint-plugin": "^8.2.0", 46 | "@typescript-eslint/parser": "^8.2.0", 47 | "eslint": "^8.57.0", 48 | "eslint-plugin-import": "^2.26.0", 49 | "eslint-plugin-prettier": "^5.0.0", 50 | "prettier": "^3.0.0", 51 | "tsup": "^6.7.0", 52 | "typescript": "5.6.x" 53 | }, 54 | "dependencies": { 55 | "@stricli/core": "^1.1.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/auto-complete/src/app.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { buildApplication, buildRouteMap } from "@stricli/core"; 4 | import { installCommand, uninstallCommand } from "./commands"; 5 | import pkg from "../package.json"; 6 | 7 | const root = buildRouteMap({ 8 | routes: { 9 | install: installCommand, 10 | uninstall: uninstallCommand, 11 | }, 12 | aliases: { 13 | i: "install", 14 | }, 15 | docs: { 16 | brief: "Manage auto-complete command installations for shells", 17 | }, 18 | }); 19 | 20 | export const app = buildApplication(root, { 21 | name: pkg.name, 22 | versionInfo: { 23 | getCurrentVersion: async () => pkg.version, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/auto-complete/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright 2024 Bloomberg Finance L.P. 3 | // Distributed under the terms of the Apache 2.0 license. 4 | import { run } from "@stricli/core"; 5 | import { app } from "../app"; 6 | void run(app, process.argv.slice(2), globalThis); 7 | -------------------------------------------------------------------------------- /packages/auto-complete/src/commands.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { buildCommand, type Command, type TypedPositionalParameter } from "@stricli/core"; 4 | import type { StricliAutoCompleteContext } from "./context"; 5 | import type { ActiveShells, ShellAutoCompleteCommands } from "./impl"; 6 | import { joinWithGrammar } from "./formatting"; 7 | 8 | const targetCommandParameter: TypedPositionalParameter = { 9 | parse: String, 10 | brief: "Target command run by user", 11 | placeholder: "targetCommand", 12 | }; 13 | 14 | export const installCommand = buildCommand({ 15 | loader: async () => { 16 | const { install } = await import("./impl"); 17 | return install; 18 | }, 19 | parameters: { 20 | flags: { 21 | bash: { 22 | kind: "parsed", 23 | brief: "Command executed by bash to generate completion proposals", 24 | parse: String, 25 | optional: true, 26 | placeholder: "command", 27 | }, 28 | }, 29 | positional: { 30 | kind: "tuple", 31 | parameters: [targetCommandParameter], 32 | }, 33 | }, 34 | docs: { 35 | brief: "Installs autocomplete support for target command on all provided shell types", 36 | }, 37 | }); 38 | 39 | export function buildInstallCommand( 40 | targetCommand: string, 41 | commands: ShellAutoCompleteCommands, 42 | ): Command { 43 | const shellsText = joinWithGrammar(Object.keys(commands), { conjunction: "and", serialComma: true }); 44 | return buildCommand({ 45 | loader: async () => { 46 | const { install } = await import("./impl"); 47 | return function () { 48 | return install.call(this, commands, targetCommand); 49 | }; 50 | }, 51 | parameters: {}, 52 | docs: { 53 | brief: `Installs ${shellsText} autocomplete support for ${targetCommand}`, 54 | }, 55 | }); 56 | } 57 | 58 | export const uninstallCommand = buildCommand({ 59 | loader: async () => { 60 | const { uninstall } = await import("./impl"); 61 | return uninstall; 62 | }, 63 | parameters: { 64 | flags: { 65 | bash: { 66 | kind: "boolean", 67 | brief: "Uninstall autocompletion for bash", 68 | optional: true, 69 | }, 70 | }, 71 | positional: { 72 | kind: "tuple", 73 | parameters: [targetCommandParameter], 74 | }, 75 | }, 76 | docs: { 77 | brief: "Uninstalls autocomplete support for target command on all selected shell types", 78 | }, 79 | }); 80 | 81 | export function buildUninstallCommand( 82 | targetCommand: string, 83 | shells: ActiveShells, 84 | ): Command { 85 | const activeShells = Object.entries(shells) 86 | .filter(([, enabled]) => enabled) 87 | .map(([shell]) => shell); 88 | const shellsText = joinWithGrammar(activeShells, { conjunction: "and", serialComma: true }); 89 | return buildCommand({ 90 | loader: async () => { 91 | const { uninstall } = await import("./impl"); 92 | return function () { 93 | return uninstall.call(this, shells, targetCommand); 94 | }; 95 | }, 96 | parameters: {}, 97 | docs: { 98 | brief: `Uninstalls ${shellsText} autocomplete support for ${targetCommand}`, 99 | }, 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /packages/auto-complete/src/context.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext } from "@stricli/core"; 4 | 5 | export interface StricliAutoCompleteContext extends CommandContext { 6 | readonly process: Pick; 7 | readonly os?: Pick; 8 | readonly fs?: { 9 | readonly promises: Pick; 10 | }; 11 | readonly path?: Pick; 12 | } 13 | -------------------------------------------------------------------------------- /packages/auto-complete/src/formatting.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | interface ConjuctiveJoin { 4 | readonly conjunction: string; 5 | readonly serialComma?: boolean; 6 | } 7 | 8 | /** 9 | * @internal 10 | */ 11 | export type JoinGrammar = ConjuctiveJoin; 12 | 13 | /** 14 | * @internal 15 | */ 16 | export function joinWithGrammar(parts: readonly string[], grammar: JoinGrammar): string { 17 | if (parts.length <= 1) { 18 | return parts[0] ?? ""; 19 | } 20 | if (parts.length === 2) { 21 | return parts.join(` ${grammar.conjunction} `); 22 | } 23 | let allButLast = parts.slice(0, parts.length - 1).join(", "); 24 | if (grammar.serialComma) { 25 | allButLast += ","; 26 | } 27 | return [allButLast, grammar.conjunction, parts[parts.length - 1]].join(" "); 28 | } 29 | -------------------------------------------------------------------------------- /packages/auto-complete/src/impl.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { StricliAutoCompleteContext } from "./context"; 4 | import { forBash } from "./shells/bash"; 5 | 6 | export interface ShellManager { 7 | readonly install: (targetCommand: string, autocompleteCommand: string) => Promise; 8 | readonly uninstall: (targetCommand: string) => Promise; 9 | } 10 | 11 | export type Shell = "bash"; 12 | 13 | export type ShellAutoCompleteCommands = Readonly>>; 14 | 15 | export async function install( 16 | this: StricliAutoCompleteContext, 17 | flags: ShellAutoCompleteCommands, 18 | targetCommand: string, 19 | ): Promise { 20 | if (flags.bash) { 21 | const bash = await forBash(this); 22 | await bash?.install(targetCommand, flags.bash); 23 | } 24 | } 25 | 26 | export type ActiveShells = Readonly>>; 27 | 28 | export async function uninstall( 29 | this: StricliAutoCompleteContext, 30 | flags: ActiveShells, 31 | targetCommand: string, 32 | ): Promise { 33 | if (flags.bash) { 34 | const shellManager = await forBash(this); 35 | await shellManager?.uninstall(targetCommand); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/auto-complete/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | export { buildInstallCommand, buildUninstallCommand } from "./commands"; 4 | export type { StricliAutoCompleteContext } from "./context"; 5 | -------------------------------------------------------------------------------- /packages/auto-complete/src/shells/bash.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { StricliAutoCompleteContext } from "../context"; 4 | import type { ShellManager } from "../impl"; 5 | import nodeOs from "node:os"; 6 | import nodeFs from "node:fs"; 7 | import nodePath from "node:path"; 8 | 9 | function getBlockStartLine(targetCommand: string): string { 10 | return `# @stricli/auto-complete START [${targetCommand}]`; 11 | } 12 | 13 | const BLOCK_END_LINE = "# @stricli/auto-complete END"; 14 | 15 | export async function forBash(context: StricliAutoCompleteContext): Promise { 16 | const { os = nodeOs, fs = nodeFs, path = nodePath } = context; 17 | if (!context.process.env["SHELL"]?.includes("bash")) { 18 | context.process.stderr.write(`Skipping bash as shell was not detected.\n`); 19 | return; 20 | } 21 | const home = os.homedir(); 22 | const bashrc = path.join(home, ".bashrc"); 23 | let lines: string[]; 24 | try { 25 | const data = await fs.promises.readFile(bashrc); 26 | lines = data.toString().split(/\n/); 27 | } catch { 28 | context.process.stderr.write(`Expected to edit ~/.bashrc but file was not found.\n`); 29 | return; 30 | } 31 | return { 32 | install: async (targetCommand, autocompleteCommand) => { 33 | const startLine = getBlockStartLine(targetCommand); 34 | const startIndex = lines.indexOf(startLine); 35 | const bashFunctionName = `__${targetCommand}_complete`; 36 | const blockLines = [ 37 | startLine, 38 | `${bashFunctionName}() { export COMP_LINE; COMPREPLY=( $(${autocompleteCommand} $COMP_LINE) ); return 0; }`, 39 | `complete -o default -o nospace -F ${bashFunctionName} ${targetCommand}`, 40 | BLOCK_END_LINE, 41 | ]; 42 | if (startIndex >= 0) { 43 | const endIndex = lines.indexOf(BLOCK_END_LINE, startIndex); 44 | lines.splice(startIndex, endIndex - startIndex + 1, ...blockLines); 45 | } else { 46 | lines.push(...blockLines); 47 | } 48 | await fs.promises.writeFile(bashrc, lines.join("\n")); 49 | context.process.stdout.write(`Restart bash shell or run \`source ~/.bashrc\` to load changes.\n`); 50 | }, 51 | uninstall: async (targetCommand) => { 52 | const startLine = getBlockStartLine(targetCommand); 53 | const startIndex = lines.indexOf(startLine); 54 | if (startIndex >= 0) { 55 | const endIndex = lines.indexOf(BLOCK_END_LINE, startIndex); 56 | lines.splice(startIndex, endIndex - startIndex + 1); 57 | } 58 | await fs.promises.writeFile(bashrc, lines.join("\n")); 59 | context.process.stdout.write(`Restart bash shell or run \`source ~/.bashrc\` to load changes.\n`); 60 | }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/auto-complete/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "rootDir": "..", 5 | "types": ["node"], 6 | "resolveJsonModule": true, 7 | "target": "esnext", 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "lib": ["esnext"], 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "isolatedModules": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitOverride": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "noUncheckedIndexedAccess": true, 18 | "verbatimModuleSyntax": true, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": ["**/*"], 22 | "exclude": [] 23 | } 24 | -------------------------------------------------------------------------------- /packages/auto-complete/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/tsconfig.json", 3 | "compilerOptions": { 4 | // Emit 5 | "emitDeclarationOnly": false, 6 | "noEmit": true, 7 | // Modules 8 | "rootDir": ".", 9 | "types": ["node", "mocha"], 10 | // Interop Constraints 11 | "esModuleInterop": true 12 | }, 13 | "exclude": ["dist/**/*", "lib/**/*"], 14 | "include": ["src/**/*", "tests/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # TypeScript 107 | lib/ 108 | 109 | # esbuild 110 | build/ 111 | 112 | tests/baselines/local 113 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # `@stricli/core` 2 | 3 | > Build complex CLIs with type safety and no dependencies 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/@stricli/core.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/core) 6 | [![NPM Type Definitions](https://img.shields.io/npm/types/@stricli/core.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/core) 7 | [![NPM Downloads](https://img.shields.io/npm/dm/@stricli/core.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/core) 8 | 9 | 👉 See **https://bloomberg.github.io/stricli** for documentation on Stricli and **https://bloomberg.github.io/stricli/packages/core** for the API docs of this package. 10 | -------------------------------------------------------------------------------- /packages/core/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import common from "../../eslint.config.mjs"; 2 | import ts from "typescript-eslint"; 3 | 4 | export default [ 5 | ...ts.configs.strictTypeChecked, 6 | ...common, 7 | { 8 | languageOptions: { 9 | parserOptions: { 10 | project: ["src/tsconfig.json"], 11 | }, 12 | }, 13 | }, 14 | { 15 | files: ["tests/**/*.ts"], 16 | languageOptions: { 17 | parserOptions: { 18 | project: ["tsconfig.json"], 19 | }, 20 | }, 21 | rules: { 22 | "no-control-regex": "off", 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stricli/core", 3 | "version": "1.1.2", 4 | "description": "Build complex CLIs with type safety and no dependencies", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bloomberg/stricli/tree/main/packages/core" 9 | }, 10 | "author": "Michael Molisani ", 11 | "files": [ 12 | "dist" 13 | ], 14 | "type": "module", 15 | "exports": { 16 | "import": "./dist/index.js", 17 | "require": "./dist/index.cjs" 18 | }, 19 | "types": "dist/index.d.ts", 20 | "scripts": { 21 | "format": "prettier --config ../../.prettierrc -w .", 22 | "format:check": "prettier --config ../../.prettierrc -c .", 23 | "lint": "eslint src tests", 24 | "lint:fix": "eslint src tests --fix", 25 | "typecheck": "tsc -p tsconfig.json --noEmit", 26 | "test": "mocha", 27 | "test:clear-baseline": "node scripts/clear_baseline", 28 | "test:accept-baseline": "node scripts/accept_baseline", 29 | "coverage": "c8 npm test", 30 | "build": "tsup", 31 | "prepublishOnly": "npm run build" 32 | }, 33 | "mocha": { 34 | "import": "tsx/esm", 35 | "spec": "tests/**/*.spec.ts" 36 | }, 37 | "c8": { 38 | "reporter": [ 39 | "text", 40 | "lcovonly" 41 | ], 42 | "check-coverage": true, 43 | "skip-full": true 44 | }, 45 | "tsup": { 46 | "entry": [ 47 | "src/index.ts" 48 | ], 49 | "format": [ 50 | "cjs", 51 | "esm" 52 | ], 53 | "tsconfig": "src/tsconfig.json", 54 | "dts": true, 55 | "minify": true, 56 | "clean": true 57 | }, 58 | "devDependencies": { 59 | "@types/chai": "^4.3.11", 60 | "@types/fs-extra": "^11.0.4", 61 | "@types/mocha": "^10.0.6", 62 | "@types/sinon": "^17.0.2", 63 | "@typescript-eslint/eslint-plugin": "^8.2.0", 64 | "@typescript-eslint/parser": "^8.2.0", 65 | "c8": "^8.0.1", 66 | "chai": "^4.3.10", 67 | "eslint": "^8.57.0", 68 | "eslint-plugin-header": "^3.1.1", 69 | "eslint-plugin-import": "^2.29.1", 70 | "eslint-plugin-prettier": "^5.1.3", 71 | "fs-extra": "^11.2.0", 72 | "mocha": "^10.2.0", 73 | "prettier": "^3.2.5", 74 | "sinon": "^17.0.1", 75 | "tsup": "^8.0.1", 76 | "tsx": "^4.8.2", 77 | "typescript": "5.6.x" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/scripts/accept_baseline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs-extra"; 3 | import path from "node:path"; 4 | import url from "node:url"; 5 | const __dirname = url.fileURLToPath(new url.URL(".", import.meta.url)); 6 | const baselinesDir = path.join(__dirname, "..", "tests", "baselines"); 7 | fs.copySync(path.join(baselinesDir, "local"), path.join(baselinesDir, "reference"), { recursive: true }); 8 | -------------------------------------------------------------------------------- /packages/core/scripts/clear_baseline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs-extra"; 3 | import path from "node:path"; 4 | import url from "node:url"; 5 | const __dirname = url.fileURLToPath(new url.URL(".", import.meta.url)); 6 | const baselinesDir = path.join(__dirname, "..", "tests", "baselines"); 7 | fs.rmSync(path.join(baselinesDir, "local"), { recursive: true }); 8 | fs.rmSync(path.join(baselinesDir, "reference"), { recursive: true }); 9 | -------------------------------------------------------------------------------- /packages/core/src/application/builder.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | /* eslint-disable @typescript-eslint/unified-signatures */ 4 | import { withDefaults, type PartialApplicationConfiguration } from "../config"; 5 | import type { CommandContext } from "../context"; 6 | import { CommandSymbol, type Command } from "../routing/command/types"; 7 | import type { RouteMap } from "../routing/route-map/types"; 8 | import type { RoutingTarget } from "../routing/types"; 9 | import { InternalError } from "../util/error"; 10 | import type { Application } from "./types"; 11 | 12 | /** 13 | * Builds an application from a top-level route map and configuration. 14 | */ 15 | export function buildApplication( 16 | root: RouteMap, 17 | config: PartialApplicationConfiguration, 18 | ): Application; 19 | /** 20 | * Builds an application with a single, top-level command and configuration. 21 | */ 22 | export function buildApplication( 23 | command: Command, 24 | appConfig: PartialApplicationConfiguration, 25 | ): Application; 26 | export function buildApplication( 27 | root: RoutingTarget, 28 | appConfig: PartialApplicationConfiguration, 29 | ): Application { 30 | const config = withDefaults(appConfig); 31 | if (root.kind === CommandSymbol && config.versionInfo) { 32 | if (root.usesFlag("version")) { 33 | throw new InternalError("Unable to use command with flag --version as root when version info is supplied"); 34 | } 35 | if (root.usesFlag("v")) { 36 | throw new InternalError("Unable to use command with alias -v as root when version info is supplied"); 37 | } 38 | } 39 | const defaultText = config.localization.loadText(config.localization.defaultLocale); 40 | if (!defaultText) { 41 | throw new InternalError(`No text available for the default locale "${config.localization.defaultLocale}"`); 42 | } 43 | return { 44 | root, 45 | config, 46 | defaultText, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/application/documentation.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { DisplayCaseStyle } from "../config"; 4 | import type { CommandContext } from "../context"; 5 | import { CommandSymbol, type Command } from "../routing/command/types"; 6 | import { RouteMapSymbol, type RouteMap } from "../routing/route-map/types"; 7 | import { InternalError } from "../util/error"; 8 | import type { Application } from "./types"; 9 | 10 | export type DocumentedCommand = readonly [route: string, helpText: string]; 11 | 12 | type CommandWithRoute = readonly [ 13 | route: readonly string[], 14 | command: Command, 15 | aliases: readonly string[], 16 | ]; 17 | 18 | function* iterateAllCommands( 19 | routeMap: RouteMap, 20 | prefix: readonly string[], 21 | caseStyle: DisplayCaseStyle, 22 | ): Generator { 23 | for (const entry of routeMap.getAllEntries()) { 24 | if (entry.hidden) { 25 | continue; 26 | } 27 | const routeName = entry.name[caseStyle]; 28 | const route = [...prefix, routeName]; 29 | if (entry.target.kind === RouteMapSymbol) { 30 | yield* iterateAllCommands(entry.target, route, caseStyle); 31 | } else { 32 | yield [route, entry.target, entry.aliases]; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Generate help text in the given locale for each command in this application. 39 | * Return value is an array of tuples containing the route to that command and the help text. 40 | * 41 | * If no locale specified, uses the defaultLocale from the application configuration. 42 | */ 43 | export function generateHelpTextForAllCommands( 44 | { root, defaultText, config }: Application, 45 | locale?: string, 46 | ): readonly DocumentedCommand[] { 47 | let text = defaultText; 48 | if (locale) { 49 | const localeText = config.localization.loadText(locale); 50 | if (localeText) { 51 | text = localeText; 52 | } else { 53 | throw new InternalError(`Application does not support "${locale}" locale`); 54 | } 55 | } 56 | const commands: CommandWithRoute[] = []; 57 | if (root.kind === CommandSymbol) { 58 | commands.push([[config.name], root, []]); 59 | } else { 60 | commands.push(...iterateAllCommands(root, [config.name], config.documentation.caseStyle)); 61 | } 62 | return commands.map(([route, command, aliases]) => { 63 | return [ 64 | route.join(" "), 65 | command.formatHelp({ 66 | prefix: route, 67 | config: config.documentation, 68 | includeVersionFlag: Boolean(config.versionInfo) && route.length === 1, 69 | includeArgumentEscapeSequenceFlag: config.scanner.allowArgumentEscapeSequence, 70 | includeHelpAllFlag: config.documentation.alwaysShowHelpAllFlag, 71 | includeHidden: false, 72 | aliases, 73 | text, 74 | ansiColor: false, 75 | }), 76 | ]; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/application/propose-completions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext, StricliDynamicCommandContext } from "../context"; 4 | import type { ArgumentCompletion } from "../parameter/scanner"; 5 | import { proposeCompletionsForCommand } from "../routing/command/propose-completions"; 6 | import { proposeCompletionsForRouteMap } from "../routing/route-map/propose-completions"; 7 | import { RouteMapSymbol } from "../routing/route-map/types"; 8 | import { buildRouteScanner, type RouteNotFoundError } from "../routing/scanner"; 9 | import type { RoutingTargetCompletion } from "../routing/types"; 10 | import type { Application } from "./types"; 11 | 12 | export type InputCompletion = ArgumentCompletion | RoutingTargetCompletion; 13 | 14 | /** 15 | * Propose possible completions for a partial input string. 16 | */ 17 | export async function proposeCompletionsForApplication( 18 | { root, config, defaultText }: Application, 19 | rawInputs: readonly string[], 20 | context: StricliDynamicCommandContext, 21 | ): Promise { 22 | if (rawInputs.length === 0) { 23 | return []; 24 | } 25 | 26 | const scanner = buildRouteScanner(root, config.scanner, []); 27 | const leadingInputs = rawInputs.slice(0, -1); 28 | let error: RouteNotFoundError | undefined; 29 | while (leadingInputs.length > 0 && !error) { 30 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 31 | const input = leadingInputs.shift()!; 32 | error = scanner.next(input); 33 | } 34 | if (error) { 35 | return []; 36 | } 37 | const result = scanner.finish(); 38 | 39 | if (result.helpRequested) { 40 | return []; 41 | } 42 | 43 | let commandContext: CONTEXT; 44 | if ("forCommand" in context) { 45 | try { 46 | commandContext = await context.forCommand({ prefix: result.prefix }); 47 | } catch { 48 | return []; 49 | } 50 | } else { 51 | commandContext = context; 52 | } 53 | 54 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 55 | const partial = rawInputs[rawInputs.length - 1]!; 56 | if (result.target.kind === RouteMapSymbol) { 57 | return proposeCompletionsForRouteMap(result.target, { 58 | context: commandContext, 59 | partial, 60 | scannerConfig: config.scanner, 61 | completionConfig: config.completion, 62 | }); 63 | } 64 | 65 | return proposeCompletionsForCommand(result.target, { 66 | context: commandContext, 67 | inputs: result.unprocessedInputs, 68 | partial, 69 | scannerConfig: config.scanner, 70 | completionConfig: config.completion, 71 | text: defaultText, 72 | includeVersionFlag: Boolean(config.versionInfo) && result.rootLevel, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/src/application/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { ApplicationConfiguration } from "../config"; 4 | import type { ApplicationText } from "../text"; 5 | import type { CommandContext } from "../context"; 6 | import type { RoutingTarget } from "../routing/types"; 7 | 8 | /** 9 | * Interface for top-level command line application. 10 | */ 11 | export interface Application { 12 | readonly root: RoutingTarget; 13 | readonly config: ApplicationConfiguration; 14 | readonly defaultText: ApplicationText; 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/exit-code.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | /** 5 | * Enumeration of all possible exit codes returned by an application. 6 | */ 7 | export const ExitCode = { 8 | /** 9 | * Unable to find a command in the application with the given command line arguments. 10 | */ 11 | UnknownCommand: -5, 12 | /** 13 | * Unable to parse the specified arguments. 14 | */ 15 | InvalidArgument: -4, 16 | /** 17 | * An error was thrown while loading the context for a command run. 18 | */ 19 | ContextLoadError: -3, 20 | /** 21 | * Failed to load command module. 22 | */ 23 | CommandLoadError: -2, 24 | /** 25 | * An unexpected error was thrown by or not caught by this library. 26 | */ 27 | InternalError: -1, 28 | /** 29 | * Command executed successfully. 30 | */ 31 | Success: 0, 32 | /** 33 | * Command module unexpectedly threw an error. 34 | */ 35 | CommandRunError: 1, 36 | } as const; 37 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { runApplication } from "./application/run"; 4 | import type { Application } from "./application/types"; 5 | import type { CommandContext, StricliDynamicCommandContext } from "./context"; 6 | 7 | export { buildApplication } from "./application/builder"; 8 | export { generateHelpTextForAllCommands } from "./application/documentation"; 9 | export type { DocumentedCommand } from "./application/documentation"; 10 | export { proposeCompletionsForApplication as proposeCompletions } from "./application/propose-completions"; 11 | export type { InputCompletion } from "./application/propose-completions"; 12 | export type { Application } from "./application/types"; 13 | export type { 14 | ApplicationConfiguration, 15 | CompletionConfiguration, 16 | DisplayCaseStyle, 17 | DocumentationConfiguration, 18 | PartialApplicationConfiguration, 19 | ScannerCaseStyle, 20 | ScannerConfiguration, 21 | VersionInfo, 22 | } from "./config"; 23 | export type { 24 | ApplicationContext, 25 | CommandContext, 26 | CommandInfo, 27 | EnvironmentVariableName, 28 | StricliCommandContextBuilder, 29 | StricliDynamicCommandContext, 30 | StricliProcess, 31 | } from "./context"; 32 | export { ExitCode } from "./exit-code"; 33 | export type { FlagParametersForType, TypedFlagParameter } from "./parameter/flag/types"; 34 | export { booleanParser, looseBooleanParser } from "./parameter/parser/boolean"; 35 | export { buildChoiceParser } from "./parameter/parser/choice"; 36 | export { numberParser } from "./parameter/parser/number"; 37 | export type { TypedPositionalParameter, TypedPositionalParameters } from "./parameter/positional/types"; 38 | export { 39 | AliasNotFoundError, 40 | ArgumentParseError, 41 | ArgumentScannerError, 42 | EnumValidationError, 43 | FlagNotFoundError, 44 | InvalidNegatedFlagSyntaxError, 45 | UnexpectedFlagError, 46 | UnexpectedPositionalError, 47 | UnsatisfiedFlagError, 48 | UnsatisfiedPositionalError, 49 | formatMessageForArgumentScannerError, 50 | } from "./parameter/scanner"; 51 | export type { 52 | Aliases, 53 | InputParser, 54 | TypedCommandFlagParameters, 55 | TypedCommandParameters, 56 | TypedCommandPositionalParameters, 57 | } from "./parameter/types"; 58 | export { buildCommand } from "./routing/command/builder"; 59 | export type { CommandBuilderArguments } from "./routing/command/builder"; 60 | export type { Command } from "./routing/command/types"; 61 | export { buildRouteMap } from "./routing/route-map/builder"; 62 | export type { RouteMapBuilderArguments } from "./routing/route-map/builder"; 63 | export type { RouteMap } from "./routing/route-map/types"; 64 | export { text_en } from "./text"; 65 | export type { 66 | ApplicationErrorFormatting, 67 | ApplicationText, 68 | DocumentationBriefs, 69 | DocumentationHeaders, 70 | DocumentationKeywords, 71 | } from "./text"; 72 | 73 | export async function run( 74 | app: Application, 75 | inputs: readonly string[], 76 | context: StricliDynamicCommandContext, 77 | ): Promise { 78 | const exitCode = await runApplication(app, inputs, context); 79 | 80 | // We set the exit code instead of calling exit() so as not 81 | // to cancel any pending tasks (e.g. stdout writes) 82 | context.process.exitCode = exitCode; 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/parameter/parser/boolean.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | /** 5 | * Parses input strings as booleans. 6 | * Transforms to lowercase and then checks against "true" and "false". 7 | */ 8 | export const booleanParser = (input: string): boolean => { 9 | switch (input.toLowerCase()) { 10 | case "true": 11 | return true; 12 | case "false": 13 | return false; 14 | } 15 | throw new SyntaxError(`Cannot convert ${input} to a boolean`); 16 | }; 17 | 18 | const TRUTHY_VALUES = new Set(["true", "t", "yes", "y", "on", "1"]); 19 | const FALSY_VALUES = new Set(["false", "f", "no", "n", "off", "0"]); 20 | 21 | /** 22 | * Parses input strings as booleans (loosely). 23 | * Transforms to lowercase and then checks against the following values: 24 | * - `true`: `"true"`, `"t"`, `"yes"`, `"y"`, `"on"`, `"1"` 25 | * - `false`: `"false"`, `"f"`, `"no"`, `"n"`, `"off"`, `"0"` 26 | */ 27 | export const looseBooleanParser = (input: string): boolean => { 28 | const value = input.toLowerCase(); 29 | if (TRUTHY_VALUES.has(value)) { 30 | return true; 31 | } 32 | if (FALSY_VALUES.has(value)) { 33 | return false; 34 | } 35 | throw new SyntaxError(`Cannot convert ${input} to a boolean`); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/core/src/parameter/parser/choice.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { InputParser } from "../types"; 4 | 5 | function narrowString(choices: readonly T[], value: string): value is T { 6 | return choices.includes(value as T); 7 | } 8 | 9 | /** 10 | * Creates an input parser that checks if the input string is found in a list of choices. 11 | */ 12 | export function buildChoiceParser(choices: readonly T[]): InputParser { 13 | return (input: string): T => { 14 | if (!narrowString(choices, input)) { 15 | throw new SyntaxError(`${input} is not one of (${choices.join("|")})`); 16 | } 17 | return input; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/parameter/parser/number.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | /** 5 | * Parses input strings as numbers. 6 | */ 7 | export const numberParser = (input: string): number => { 8 | const value = Number(input); 9 | if (Number.isNaN(value)) { 10 | throw new SyntaxError(`Cannot convert ${input} to a number`); 11 | } 12 | return value; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/core/src/parameter/positional/formatting.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { HelpFormattingArguments } from "../../routing/types"; 4 | import { formatRowsWithColumns } from "../../util/formatting"; 5 | import type { PositionalParameter, PositionalParameters } from "./types"; 6 | 7 | /** 8 | * @internal 9 | */ 10 | export function formatDocumentationForPositionalParameters( 11 | positional: PositionalParameters, 12 | args: Pick, 13 | ): readonly string[] { 14 | if (positional.kind === "array") { 15 | const name = positional.parameter.placeholder ?? "args"; 16 | const argName = args.ansiColor ? `\x1B[97m${name}...\x1B[39m` : `${name}...`; 17 | const brief = args.ansiColor ? `\x1B[3m${positional.parameter.brief}\x1B[23m` : positional.parameter.brief; 18 | return formatRowsWithColumns([[argName, brief]], [" "]); 19 | } 20 | const { keywords } = args.text; 21 | const atLeastOneOptional = positional.parameters.some((def) => def.optional); 22 | return formatRowsWithColumns( 23 | positional.parameters.map((def: PositionalParameter, i) => { 24 | let name = def.placeholder ?? `arg${i + 1}`; 25 | let suffix: string | undefined; 26 | if (def.optional) { 27 | name = `[${name}]`; 28 | } else if (atLeastOneOptional) { 29 | name = ` ${name}`; 30 | } 31 | if (def.default) { 32 | const defaultKeyword = args.ansiColor ? `\x1B[90m${keywords.default}\x1B[39m` : keywords.default; 33 | suffix = `[${defaultKeyword} ${def.default}]`; 34 | } 35 | return [ 36 | args.ansiColor ? `\x1B[97m${name}\x1B[39m` : name, 37 | args.ansiColor ? `\x1B[3m${def.brief}\x1B[23m` : def.brief, 38 | suffix ?? "", 39 | ]; 40 | }), 41 | [" ", " "], 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/parameter/positional/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext } from "../../context"; 4 | import type { ParsedParameter } from "../types"; 5 | 6 | interface BasePositionalParameter extends ParsedParameter { 7 | /** 8 | * In-line documentation for this parameter. 9 | */ 10 | readonly brief: string; 11 | /** 12 | * String that serves as placeholder for the value in the generated usage line. 13 | * Defaults to "argN" where N is the index of this parameter. 14 | */ 15 | readonly placeholder?: string; 16 | /** 17 | * Default input value if one is not provided at runtime. 18 | */ 19 | readonly default?: string; 20 | readonly optional?: boolean; 21 | } 22 | 23 | interface RequiredPositionalParameter extends BasePositionalParameter { 24 | /** 25 | * Parameter is required and cannot be set as optional. 26 | */ 27 | readonly optional?: false; 28 | } 29 | 30 | interface OptionalPositionalParameter extends BasePositionalParameter { 31 | /** 32 | * Parameter is optional and must be specified as such. 33 | */ 34 | readonly optional: true; 35 | } 36 | 37 | /** 38 | * Definition of a positional parameter that will eventually be parsed to an argument. 39 | * Required properties may vary depending on the type argument `T`. 40 | */ 41 | export type TypedPositionalParameter = undefined extends T 42 | ? OptionalPositionalParameter, CONTEXT> 43 | : RequiredPositionalParameter; 44 | 45 | export type PositionalParameter = BasePositionalParameter; 46 | 47 | interface PositionalParameterArray { 48 | readonly kind: "array"; 49 | readonly parameter: TypedPositionalParameter; 50 | readonly minimum?: number; 51 | readonly maximum?: number; 52 | } 53 | 54 | type PositionalParametersForTuple = { 55 | readonly [K in keyof T]: TypedPositionalParameter; 56 | }; 57 | 58 | interface PositionalParameterTuple { 59 | readonly kind: "tuple"; 60 | readonly parameters: T; 61 | } 62 | 63 | export type BaseArgs = readonly unknown[]; 64 | 65 | /** 66 | * Definition of all positional parameters. 67 | * Required properties may vary depending on the type argument `T`. 68 | */ 69 | export type TypedPositionalParameters = [T] extends [readonly (infer E)[]] 70 | ? number extends T["length"] 71 | ? PositionalParameterArray 72 | : PositionalParameterTuple> 73 | : PositionalParameterTuple>; 74 | 75 | /** 76 | * Definition of all positional parameters. 77 | * This is a separate version of {@link TypedPositionalParameters} without a type parameter and is primarily used 78 | * internally and should only be used after the types are checked. 79 | */ 80 | export type PositionalParameters = 81 | | PositionalParameterArray 82 | | PositionalParameterTuple; 83 | -------------------------------------------------------------------------------- /packages/core/src/parameter/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext } from "../context"; 4 | import type { FlagParameters, FlagParametersForType } from "./flag/types"; 5 | import type { BaseArgs, PositionalParameters, TypedPositionalParameters } from "./positional/types"; 6 | 7 | /** 8 | * Generic function that synchronously or asynchronously parses a string to an arbitrary type. 9 | */ 10 | export type InputParser = ( 11 | this: CONTEXT, 12 | input: string, 13 | ) => T | Promise; 14 | 15 | export interface ParsedParameter { 16 | /** 17 | * Function to parse an input string to the type of this parameter. 18 | */ 19 | readonly parse: InputParser; 20 | /** 21 | * Propose possible completions for a partial input string. 22 | */ 23 | readonly proposeCompletions?: (this: CONTEXT, partial: string) => readonly string[] | Promise; 24 | } 25 | 26 | type LowercaseLetter = 27 | | "a" 28 | | "b" 29 | | "c" 30 | | "d" 31 | | "e" 32 | | "f" 33 | | "g" 34 | | "h" 35 | | "i" 36 | | "j" 37 | | "k" 38 | | "l" 39 | | "m" 40 | | "n" 41 | | "o" 42 | | "p" 43 | | "q" 44 | | "r" 45 | | "s" 46 | | "t" 47 | | "u" 48 | | "v" 49 | | "w" 50 | | "x" 51 | | "y" 52 | | "z"; 53 | 54 | type UppercaseLetter = Capitalize; 55 | 56 | type ReservedAlias = "h"; 57 | 58 | export type AvailableAlias = Exclude; 59 | 60 | export type Aliases = Readonly>>; 61 | 62 | export type BaseFlags = Readonly>; 63 | 64 | interface TypedCommandFlagParameters_ { 65 | /** 66 | * Typed definitions for all flag parameters. 67 | */ 68 | readonly flags: FlagParametersForType; 69 | /** 70 | * Object that aliases single characters to flag names. 71 | */ 72 | readonly aliases?: Aliases; 73 | } 74 | 75 | export type TypedCommandFlagParameters = 76 | Record extends FLAGS 77 | ? Partial> 78 | : TypedCommandFlagParameters_; 79 | 80 | interface TypedCommandPositionalParameters_ { 81 | /** 82 | * Typed definitions for all positional parameters. 83 | */ 84 | readonly positional: TypedPositionalParameters; 85 | } 86 | 87 | export type TypedCommandPositionalParameters = [] extends ARGS 88 | ? Partial> 89 | : TypedCommandPositionalParameters_; 90 | 91 | /** 92 | * Definitions for all parameters requested by the corresponding command. 93 | */ 94 | export type TypedCommandParameters< 95 | FLAGS extends BaseFlags, 96 | ARGS extends BaseArgs, 97 | CONTEXT extends CommandContext, 98 | > = TypedCommandFlagParameters & TypedCommandPositionalParameters; 99 | 100 | /** 101 | * Definitions for all parameters requested by the corresponding command. 102 | * This is a separate version of {@link TypedCommandParameters} without a type parameter and is primarily used 103 | * internally and should only be used after the types are checked. 104 | */ 105 | export interface CommandParameters { 106 | readonly flags?: FlagParameters; 107 | readonly aliases?: Aliases; 108 | readonly positional?: PositionalParameters; 109 | } 110 | -------------------------------------------------------------------------------- /packages/core/src/routing/command/documentation.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { formatDocumentationForFlagParameters, generateBuiltInFlagUsageLines } from "../../parameter/flag/formatting"; 4 | import { formatUsageLineForParameters } from "../../parameter/formatting"; 5 | import { formatDocumentationForPositionalParameters } from "../../parameter/positional/formatting"; 6 | import type { CommandParameters } from "../../parameter/types"; 7 | import type { HelpFormattingArguments } from "../types"; 8 | 9 | export interface CustomUsage { 10 | readonly input: string; 11 | readonly brief: string; 12 | } 13 | 14 | export interface CommandDocumentation { 15 | /** 16 | * In-line documentation for this command. 17 | */ 18 | readonly brief: string; 19 | /** 20 | * Longer description of this command's behavior, only printed during `--help`. 21 | */ 22 | readonly fullDescription?: string; 23 | /** 24 | * Sample usage to replace the generated usage lines. 25 | */ 26 | readonly customUsage?: readonly (string | CustomUsage)[]; 27 | } 28 | 29 | /** 30 | * @internal 31 | */ 32 | export function* generateCommandHelpLines( 33 | parameters: CommandParameters, 34 | docs: CommandDocumentation, 35 | args: HelpFormattingArguments, 36 | ): Generator { 37 | const { brief, fullDescription, customUsage } = docs; 38 | const { headers } = args.text; 39 | const prefix = args.prefix.join(" "); 40 | yield args.ansiColor ? `\x1B[1m${headers.usage}\x1B[22m` : headers.usage; 41 | if (customUsage) { 42 | for (const usage of customUsage) { 43 | if (typeof usage === "string") { 44 | yield ` ${prefix} ${usage}`; 45 | } else { 46 | const brief = args.ansiColor ? `\x1B[3m${usage.brief}\x1B[23m` : usage.brief; 47 | yield ` ${prefix} ${usage.input}\n ${brief}`; 48 | } 49 | } 50 | } else { 51 | yield ` ${formatUsageLineForParameters(parameters, args)}`; 52 | } 53 | for (const line of generateBuiltInFlagUsageLines(args)) { 54 | yield ` ${prefix} ${line}`; 55 | } 56 | yield ""; 57 | yield fullDescription ?? brief; 58 | if (args.aliases && args.aliases.length > 0) { 59 | const aliasPrefix = args.prefix.slice(0, -1).join(" "); 60 | yield ""; 61 | yield args.ansiColor ? `\x1B[1m${headers.aliases}\x1B[22m` : headers.aliases; 62 | for (const alias of args.aliases) { 63 | yield ` ${aliasPrefix} ${alias}`; 64 | } 65 | } 66 | yield ""; 67 | yield args.ansiColor ? `\x1B[1m${headers.flags}\x1B[22m` : headers.flags; 68 | for (const line of formatDocumentationForFlagParameters(parameters.flags ?? {}, parameters.aliases ?? {}, args)) { 69 | yield ` ${line}`; 70 | } 71 | const positional = parameters.positional ?? { kind: "tuple", parameters: [] }; 72 | if (positional.kind === "array" || positional.parameters.length > 0) { 73 | yield ""; 74 | yield args.ansiColor ? `\x1B[1m${headers.arguments}\x1B[22m` : headers.arguments; 75 | for (const line of formatDocumentationForPositionalParameters(positional, args)) { 76 | yield ` ${line}`; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/routing/command/propose-completions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { ScannerConfiguration } from "../../config"; 4 | import type { CommandContext } from "../../context"; 5 | import { 6 | buildArgumentScanner, 7 | type ArgumentCompletion, 8 | type ArgumentScannerCompletionArguments, 9 | } from "../../parameter/scanner"; 10 | import type { Command } from "./types"; 11 | 12 | export interface CommandProposeCompletionsArguments 13 | extends ArgumentScannerCompletionArguments { 14 | readonly inputs: readonly string[]; 15 | readonly scannerConfig: ScannerConfiguration; 16 | } 17 | 18 | export async function proposeCompletionsForCommand( 19 | { parameters }: Command, 20 | args: CommandProposeCompletionsArguments, 21 | ): Promise { 22 | try { 23 | const scanner = buildArgumentScanner(parameters, args.scannerConfig); 24 | for (const input of args.inputs) { 25 | scanner.next(input); 26 | } 27 | return await scanner.proposeCompletions(args); 28 | } catch { 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/routing/command/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext } from "../../context"; 4 | import type { BaseArgs } from "../../parameter/positional/types"; 5 | import type { BaseFlags, CommandParameters } from "../../parameter/types"; 6 | import type { DocumentedTarget } from "../types"; 7 | 8 | /** 9 | * All command functions are required to have a general signature: 10 | * ```ts 11 | * (flags: {...}, ...args: [...]) => void | Promise 12 | * ``` 13 | * - `args` should be an array/tuple of any length or type. 14 | * - `flags` should be an object with any key-value pairs. 15 | * 16 | * The specific types of `args` and `flags` are customizable to the individual use case 17 | * and will be used to determine the structure of the positional arguments and flags. 18 | */ 19 | export type CommandFunction = ( 20 | this: CONTEXT, 21 | flags: FLAGS, 22 | ...args: ARGS 23 | ) => void | Error | Promise; 24 | 25 | /** 26 | * A command module exposes the target function as the default export. 27 | */ 28 | export interface CommandModule { 29 | readonly default: CommandFunction; 30 | } 31 | 32 | /** 33 | * Asynchronously loads a function or a module containing a function to be executed by a command. 34 | */ 35 | export type CommandFunctionLoader< 36 | FLAGS extends BaseFlags, 37 | ARGS extends BaseArgs, 38 | CONTEXT extends CommandContext, 39 | > = () => Promise | CommandFunction>; 40 | 41 | export const CommandSymbol = Symbol("Command"); 42 | 43 | /** 44 | * Parsed and validated command instance. 45 | */ 46 | export interface Command extends DocumentedTarget { 47 | readonly kind: typeof CommandSymbol; 48 | readonly loader: CommandFunctionLoader; 49 | readonly parameters: CommandParameters; 50 | readonly usesFlag: (flagName: string) => boolean; 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/src/routing/route-map/propose-completions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CompletionConfiguration, ScannerConfiguration } from "../../config"; 4 | import type { CommandContext } from "../../context"; 5 | import { CommandSymbol } from "../command/types"; 6 | import type { RoutingTargetCompletion } from "../types"; 7 | import type { RouteMap } from "./types"; 8 | 9 | export interface RouteMapCompletionArguments { 10 | readonly context: CONTEXT; 11 | readonly partial: string; 12 | readonly scannerConfig: ScannerConfiguration; 13 | readonly completionConfig: CompletionConfiguration; 14 | } 15 | 16 | export async function proposeCompletionsForRouteMap( 17 | routeMap: RouteMap, 18 | { partial, scannerConfig, completionConfig }: RouteMapCompletionArguments, 19 | ): Promise { 20 | let entries = routeMap.getAllEntries(); 21 | if (!completionConfig.includeHiddenRoutes) { 22 | entries = entries.filter((entry) => !entry.hidden); 23 | } 24 | const displayCaseStyle = 25 | scannerConfig.caseStyle === "allow-kebab-for-camel" ? "convert-camel-to-kebab" : scannerConfig.caseStyle; 26 | return entries 27 | .flatMap((entry) => { 28 | const kind = entry.target.kind === CommandSymbol ? "routing-target:command" : "routing-target:route-map"; 29 | const brief = entry.target.brief; 30 | const targetCompletion: RoutingTargetCompletion = { 31 | kind, 32 | completion: entry.name[displayCaseStyle], 33 | brief, 34 | }; 35 | if (completionConfig.includeAliases) { 36 | return [ 37 | targetCompletion, 38 | ...entry.aliases.map((alias) => { 39 | return { 40 | kind, 41 | completion: alias, 42 | brief, 43 | }; 44 | }), 45 | ]; 46 | } 47 | return [targetCompletion]; 48 | }) 49 | .filter(({ completion }) => completion.startsWith(partial)); 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/routing/route-map/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { DisplayCaseStyle, ScannerCaseStyle } from "../../config"; 4 | import type { CommandContext } from "../../context"; 5 | import type { Command } from "../command/types"; 6 | import type { DocumentedTarget, RoutingTarget } from "../types"; 7 | 8 | export const RouteMapSymbol = Symbol("RouteMap"); 9 | 10 | export interface RouteMapEntry { 11 | readonly name: Readonly>; 12 | readonly target: RoutingTarget; 13 | readonly aliases: readonly string[]; 14 | readonly hidden: boolean; 15 | } 16 | 17 | /** 18 | * Route map that stores multiple routes. 19 | */ 20 | export interface RouteMap extends DocumentedTarget { 21 | readonly kind: typeof RouteMapSymbol; 22 | readonly getRoutingTargetForInput: (input: string) => RoutingTarget | undefined; 23 | readonly getDefaultCommand: () => Command | undefined; 24 | readonly getOtherAliasesForInput: ( 25 | input: string, 26 | caseStyle: ScannerCaseStyle, 27 | ) => Readonly>; 28 | readonly getAllEntries: () => readonly RouteMapEntry[]; 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/routing/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { DocumentationText } from "../text"; 4 | import type { CommandContext } from "../context"; 5 | import type { UsageFormattingArguments } from "../parameter/formatting"; 6 | import type { Command } from "./command/types"; 7 | import type { RouteMap } from "./route-map/types"; 8 | 9 | export interface HelpFormattingArguments extends UsageFormattingArguments { 10 | readonly text: DocumentationText; 11 | readonly includeHelpAllFlag: boolean; 12 | readonly includeVersionFlag: boolean; 13 | readonly includeArgumentEscapeSequenceFlag: boolean; 14 | readonly includeHidden: boolean; 15 | readonly aliases?: readonly string[]; 16 | } 17 | 18 | export interface DocumentedTarget { 19 | readonly brief: string; 20 | readonly formatUsageLine: (args: UsageFormattingArguments) => string; 21 | readonly formatHelp: (args: HelpFormattingArguments) => string; 22 | } 23 | 24 | export type RoutingTarget = Command | RouteMap; 25 | 26 | export interface RoutingTargetCompletion { 27 | readonly kind: "routing-target:command" | "routing-target:route-map"; 28 | readonly completion: string; 29 | readonly brief: string; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Emit 4 | "noEmit": true, 5 | "stripInternal": true, 6 | // Modules 7 | "rootDir": ".", 8 | "types": [], 9 | // Language and Environment 10 | "target": "esnext", 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "lib": ["esnext"], 14 | // Completeness 15 | "skipLibCheck": true, 16 | // Type Checking 17 | "strict": true, 18 | "isolatedModules": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "noImplicitOverride": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noUncheckedIndexedAccess": true, 23 | "verbatimModuleSyntax": true 24 | }, 25 | "exclude": [], 26 | "include": ["**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/util/array.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-return */ 4 | 5 | export function group(array: readonly T[], callback: (item: T) => K): Partial> { 6 | return array.reduce>>((groupings, item) => { 7 | const key = callback(item); 8 | const groupItems = groupings[key] ?? []; 9 | groupItems.push(item); 10 | groupings[key] = groupItems; 11 | return groupings; 12 | }, {}); 13 | } 14 | 15 | type GroupsBySelector> = { 16 | [K in T as K[S]]?: K[]; 17 | }; 18 | 19 | export function groupBy>( 20 | array: readonly T[], 21 | selector: S, 22 | ): GroupsBySelector { 23 | return group(array, (item) => item[selector]) as unknown as GroupsBySelector; 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/util/case-style.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | 4 | /** 5 | * @internal 6 | */ 7 | export function convertKebabCaseToCamelCase(str: string): string { 8 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 9 | return str.replace(/-./g, (match) => match[1]!.toUpperCase()); 10 | } 11 | 12 | /** 13 | * @internal 14 | */ 15 | export function convertCamelCaseToKebabCase(name: string): string { 16 | return Array.from(name) 17 | .map((char, i) => { 18 | const upper = char.toUpperCase(); 19 | const lower = char.toLowerCase(); 20 | if (i === 0 || upper !== char || upper === lower) { 21 | return char; 22 | } 23 | return `-${lower}`; 24 | }) 25 | .join(""); 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/util/error.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | export class InternalError extends Error {} 4 | 5 | export function formatException(exc: unknown): string { 6 | if (exc instanceof Error) { 7 | return exc.stack ?? String(exc); 8 | } 9 | return String(exc); 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/util/formatting.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | function maximum(arr1: readonly number[], arr2: readonly number[]): readonly number[] { 4 | const maxValues: number[] = []; 5 | const maxLength = Math.max(arr1.length, arr2.length); 6 | for (let i = 0; i < maxLength; ++i) { 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 8 | maxValues[i] = Math.max(arr1[i]!, arr2[i]!); 9 | } 10 | return maxValues; 11 | } 12 | 13 | type CellRow = readonly string[]; 14 | 15 | /** 16 | * @internal 17 | */ 18 | export function formatRowsWithColumns(cells: readonly CellRow[], separators?: readonly string[]): readonly string[] { 19 | if (cells.length === 0) { 20 | return []; 21 | } 22 | const startingLengths = (Array(Math.max(...cells.map((cellRow) => cellRow.length))) as number[]).fill(0, 0); 23 | const maxLengths = cells.reduce((acc, cellRow) => { 24 | const lengths = cellRow.map((cell) => cell.length); 25 | return maximum(acc, lengths); 26 | }, startingLengths); 27 | return cells.map((cellRow) => { 28 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 29 | const firstCell = (cellRow[0] ?? "").padEnd(maxLengths[0]!); 30 | return cellRow 31 | .slice(1) 32 | .reduce( 33 | (parts, str, i, arr) => { 34 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 35 | const paddedStr = arr.length === i + 1 ? str : str.padEnd(maxLengths[i + 1]!); 36 | return [...parts, separators?.[i] ?? " ", paddedStr]; 37 | }, 38 | [firstCell], 39 | ) 40 | .join("") 41 | .trimEnd(); 42 | }); 43 | } 44 | 45 | interface ConjuctiveJoin { 46 | readonly kind: "conjunctive"; 47 | readonly conjunction: string; 48 | readonly serialComma?: boolean; 49 | } 50 | 51 | /** 52 | * @internal 53 | */ 54 | export type JoinGrammar = ConjuctiveJoin; 55 | 56 | /** 57 | * @internal 58 | */ 59 | export function joinWithGrammar(parts: readonly string[], grammar: JoinGrammar): string { 60 | if (parts.length <= 1) { 61 | return parts[0] ?? ""; 62 | } 63 | if (parts.length === 2) { 64 | return parts.join(` ${grammar.conjunction} `); 65 | } 66 | let allButLast = parts.slice(0, parts.length - 1).join(", "); 67 | if (grammar.serialComma) { 68 | allButLast += ","; 69 | } 70 | return [allButLast, grammar.conjunction, parts[parts.length - 1]].join(" "); 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/util/promise.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { groupBy } from "./array"; 4 | 5 | type PromiseAll = { [P in keyof T]: T[P] | Promise }; 6 | 7 | interface MultiplePromiseRejectedResult { 8 | readonly status: "rejected"; 9 | readonly reasons: readonly unknown[]; 10 | } 11 | 12 | export type PromiseSettledOrElseResult = PromiseFulfilledResult | MultiplePromiseRejectedResult; 13 | 14 | export async function allSettledOrElse( 15 | values: PromiseAll, 16 | ): Promise> { 17 | const results = await Promise.allSettled(values); 18 | const grouped = groupBy(results, "status"); 19 | if (grouped.rejected && grouped.rejected.length > 0) { 20 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 21 | return { status: "rejected", reasons: grouped.rejected.map((result) => result.reason) }; 22 | } 23 | 24 | return { status: "fulfilled", value: (grouped.fulfilled?.map((result) => result.value) ?? []) as unknown as T }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/tests/baselines/reference/parameter/formatting.txt: -------------------------------------------------------------------------------- 1 | :::: formatUsageLineForParameters / flags / boolean / optional boolean flag 2 | cli [--optionalBoolean] 3 | :::: formatUsageLineForParameters / flags / boolean / required boolean flag 4 | cli (--requiredBoolean) 5 | :::: formatUsageLineForParameters / flags / boolean / required boolean flag with default 6 | cli [--requiredBoolean] 7 | :::: formatUsageLineForParameters / flags / enum / optional enum flag 8 | cli [--optionalEnum a|b|c] 9 | :::: formatUsageLineForParameters / flags / enum / optional enum flag with default 10 | cli [--optionalEnum a|b|c] 11 | :::: formatUsageLineForParameters / flags / enum / required enum flag 12 | cli (--requiredEnum a|b|c) 13 | :::: formatUsageLineForParameters / flags / enum / required enum flag with default 14 | cli [--requiredEnum a|b|c] 15 | :::: formatUsageLineForParameters / flags / parsed / optional array parsed flag 16 | cli [--optionalArrayParsed value] 17 | :::: formatUsageLineForParameters / flags / parsed / optional parsed flag 18 | cli [--optionalParsed value] 19 | :::: formatUsageLineForParameters / flags / parsed / optional parsed flag [hidden] 20 | cli 21 | :::: formatUsageLineForParameters / flags / parsed / optional parsed flag with placeholder 22 | cli [--optionalParsed parsed] 23 | :::: formatUsageLineForParameters / flags / parsed / optional variadic parsed flag 24 | cli [--optionalVariadicParsed value]... 25 | :::: formatUsageLineForParameters / flags / parsed / required array parsed flag 26 | cli (--requiredArrayParsed value) 27 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag 28 | cli (--requiredParsed value) 29 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with alias 30 | cli (-p value) 31 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with default 32 | cli [--requiredParsed value] 33 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with default [hidden] 34 | cli 35 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with multiple aliases 36 | cli (--requiredParsed value) 37 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with no aliases 38 | cli (--requiredParsed value) 39 | :::: formatUsageLineForParameters / flags / parsed / required parsed flag with unused alias 40 | cli (--requiredParsed value) 41 | :::: formatUsageLineForParameters / flags / parsed / required variadic parsed flag 42 | cli (--requiredVariadicParsed value)... 43 | :::: formatUsageLineForParameters / positional parameters / homogenous array of positional parameters 44 | cli ... 45 | :::: formatUsageLineForParameters / positional parameters / tuple of mixed positional parameters 46 | cli [] [] 47 | :::: formatUsageLineForParameters / positional parameters / tuple of one optional positional parameter 48 | cli [] 49 | :::: formatUsageLineForParameters / positional parameters / tuple of one required positional parameter 50 | cli 51 | :::: formatUsageLineForParameters / positional parameters / tuple with no positional parameters 52 | cli -------------------------------------------------------------------------------- /packages/core/tests/baselines/reference/parameter/positional/formatting.txt: -------------------------------------------------------------------------------- 1 | :::: formatDocumentationForPositionalParameters / homogenous array of positional parameters / no ANSI color 2 | parsed... required positional parameter 3 | :::: formatDocumentationForPositionalParameters / homogenous array of positional parameters / with ANSI color 4 | parsed... required positional parameter 5 | :::: formatDocumentationForPositionalParameters / tuple of multiple positional parameters / no ANSI color 6 | parsed required positional parameter 7 | parsedLonger required positional parameter with longer placeholder 8 | parsed-kebab-case required positional parameter with kebab-case placeholder 9 | :::: formatDocumentationForPositionalParameters / tuple of multiple positional parameters / with ANSI color 10 | parsed required positional parameter 11 | parsedLonger required positional parameter with longer placeholder 12 | parsed-kebab-case required positional parameter with kebab-case placeholder 13 | :::: formatDocumentationForPositionalParameters / tuple of one optional positional parameter / no ANSI color 14 | [parsed] optional positional parameter 15 | :::: formatDocumentationForPositionalParameters / tuple of one optional positional parameter / with ANSI color 16 | [parsed] optional positional parameter 17 | :::: formatDocumentationForPositionalParameters / tuple of one optional positional parameter with default / no ANSI color 18 | [parsed] optional positional parameter [default = 1001] 19 | :::: formatDocumentationForPositionalParameters / tuple of one optional positional parameter with default / with ANSI color 20 | [parsed] optional positional parameter [default = 1001] 21 | :::: formatDocumentationForPositionalParameters / tuple of one positional parameter with default / no ANSI color 22 | parsed required positional parameter [default = 1001] 23 | :::: formatDocumentationForPositionalParameters / tuple of one positional parameter with default / with ANSI color 24 | parsed required positional parameter [default = 1001] 25 | :::: formatDocumentationForPositionalParameters / tuple of one positional parameter with default, with alt text / no ANSI color 26 | parsed required positional parameter [def = 1001] 27 | :::: formatDocumentationForPositionalParameters / tuple of one positional parameter with default, with alt text / with ANSI color 28 | parsed required positional parameter [def = 1001] 29 | :::: formatDocumentationForPositionalParameters / tuple of one required positional parameter / no ANSI color 30 | parsed required positional parameter 31 | :::: formatDocumentationForPositionalParameters / tuple of one required positional parameter / with ANSI color 32 | parsed required positional parameter 33 | :::: formatDocumentationForPositionalParameters / tuple with no positional parameters / no ANSI color 34 | :::: formatDocumentationForPositionalParameters / tuple with no positional parameters / with ANSI color -------------------------------------------------------------------------------- /packages/core/tests/fakes/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { type SinonStubbedInstance, stub } from "sinon"; 4 | import { type ApplicationText, text_en } from "../../src"; 5 | 6 | export function buildFakeApplicationText(): SinonStubbedInstance { 7 | return { 8 | ...text_en, 9 | noCommandRegisteredForInput: 10 | stub<[{ input: string; corrections: readonly string[]; ansiColor: boolean }]>().returns( 11 | "noCommandRegisteredForInput", 12 | ), 13 | noTextAvailableForLocale: 14 | stub<[{ requestedLocale: string; defaultLocale: string; ansiColor: boolean }]>().returns( 15 | "noTextAvailableForLocale", 16 | ), 17 | currentVersionIsNotLatest: 18 | stub<[{ currentVersion: string; latestVersion: string; ansiColor: boolean }]>().returns( 19 | "currentVersionIsNotLatest", 20 | ), 21 | exceptionWhileParsingArguments: stub<[unknown, boolean]>().returns("exceptionWhileParsingArguments"), 22 | exceptionWhileLoadingCommandFunction: stub<[unknown, boolean]>().returns( 23 | "exceptionWhileLoadingCommandFunction", 24 | ), 25 | exceptionWhileLoadingCommandContext: stub<[unknown, boolean]>().returns("exceptionWhileLoadingCommandContext"), 26 | exceptionWhileRunningCommand: stub<[unknown, boolean]>().returns("exceptionWhileRunningCommand"), 27 | commandErrorResult: stub<[Error, boolean]>().returns("commandErrorResult"), 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/tests/fakes/context.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { stub, type SinonStub } from "sinon"; 4 | import type { 5 | CommandContext, 6 | CommandInfo, 7 | EnvironmentVariableName, 8 | StricliDynamicCommandContext, 9 | StricliProcess, 10 | } from "../../src"; 11 | 12 | interface FakeWritable { 13 | readonly write: SinonStub<[string], void>; 14 | } 15 | interface FakeProcess extends StricliProcess { 16 | readonly stdout: FakeWritable; 17 | readonly stderr: FakeWritable; 18 | readonly exit: (code: number) => void; 19 | } 20 | 21 | export type FakeContext = StricliDynamicCommandContext & { 22 | readonly process: FakeProcess; 23 | forCommand?: SinonStub<[CommandInfo], FakeContext>; 24 | }; 25 | 26 | export interface FakeContextOptions { 27 | readonly forCommand?: boolean | (() => never); 28 | readonly locale?: string; 29 | readonly colorDepth?: number; 30 | readonly env?: Partial>; 31 | } 32 | 33 | export function buildFakeContext(options: FakeContextOptions = { forCommand: true, colorDepth: 4 }): FakeContext { 34 | const colorDepth = options.colorDepth; 35 | let exitCode!: number; 36 | const context: FakeContext = { 37 | process: { 38 | stdout: { 39 | write: stub(), 40 | ...(colorDepth 41 | ? { 42 | getColorDepth() { 43 | return colorDepth; 44 | }, 45 | } 46 | : {}), 47 | }, 48 | stderr: { 49 | write: stub(), 50 | ...(colorDepth 51 | ? { 52 | getColorDepth() { 53 | return colorDepth; 54 | }, 55 | } 56 | : {}), 57 | }, 58 | env: options.env, 59 | exit: (code) => { 60 | exitCode = code; 61 | }, 62 | }, 63 | locale: options.locale, 64 | }; 65 | if (options.forCommand) { 66 | if (typeof options.forCommand === "function") { 67 | context.forCommand = stub<[CommandInfo]>().callsFake(options.forCommand); 68 | } else { 69 | context.forCommand = stub<[CommandInfo]>().returns(context); 70 | } 71 | } 72 | return context; 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/tests/parameter/parser.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import assert from "assert"; 4 | import { expect } from "chai"; 5 | import type { CommandContext, InputParser } from "../../src"; 6 | 7 | type ParserTestCase = readonly [input: string, value: T, context?: CommandContext]; 8 | 9 | export function testParser(parse: InputParser, tests: readonly ParserTestCase[]) { 10 | for (const test of tests) { 11 | it(`parses ${test[0]}`, async () => { 12 | const value = await parse.call(test[2] ?? { process }, test[0]); 13 | expect(value).to.deep.equal(test[1]); 14 | }); 15 | } 16 | } 17 | 18 | type ParserErrorTestCase = readonly [input: string, message: string, context?: CommandContext]; 19 | 20 | export function testParserError(parse: InputParser, tests: readonly ParserErrorTestCase[]) { 21 | for (const test of tests) { 22 | it(`fails to parse ${test[0]}`, async () => { 23 | try { 24 | const parsed = Promise.resolve(parse.call(test[2] ?? { process }, test[0])); 25 | await parsed.then( 26 | () => { 27 | throw new Error(`Expected parse to throw with message=${test[1]}`); 28 | }, 29 | (exc: unknown) => { 30 | assert(exc instanceof Error); 31 | expect(exc).to.have.property("message", test[1]); 32 | }, 33 | ); 34 | } catch (exc) { 35 | assert(exc instanceof Error); 36 | expect(exc).to.have.property("message", test[1]); 37 | } 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/tests/parameter/parsers/boolean.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { booleanParser, looseBooleanParser } from "../../../src"; 4 | import { testParser, testParserError } from "../parser"; 5 | 6 | describe("boolean parser", () => { 7 | testParser(booleanParser, [ 8 | ["false", false], 9 | ["False", false], 10 | ["FALSE", false], 11 | ["true", true], 12 | ["True", true], 13 | ["TRUE", true], 14 | ]); 15 | 16 | testParserError(booleanParser, [ 17 | ["t", "Cannot convert t to a boolean"], 18 | ["f", "Cannot convert f to a boolean"], 19 | ["yes", "Cannot convert yes to a boolean"], 20 | ["no", "Cannot convert no to a boolean"], 21 | ["0", "Cannot convert 0 to a boolean"], 22 | ["1", "Cannot convert 1 to a boolean"], 23 | ["off", "Cannot convert off to a boolean"], 24 | ["on", "Cannot convert on to a boolean"], 25 | ]); 26 | }); 27 | 28 | describe("loose boolean parser", () => { 29 | testParser(looseBooleanParser, [ 30 | ["false", false], 31 | ["False", false], 32 | ["FALSE", false], 33 | ["true", true], 34 | ["True", true], 35 | ["TRUE", true], 36 | ["yes", true], 37 | ["Yes", true], 38 | ["YES", true], 39 | ["y", true], 40 | ["Y", true], 41 | ["no", false], 42 | ["No", false], 43 | ["NO", false], 44 | ["n", false], 45 | ["N", false], 46 | ["t", true], 47 | ["T", true], 48 | ["f", false], 49 | ["F", false], 50 | ["0", false], 51 | ["1", true], 52 | ["on", true], 53 | ["ON", true], 54 | ["On", true], 55 | ["off", false], 56 | ["OFF", false], 57 | ["Off", false], 58 | ]); 59 | 60 | testParserError(looseBooleanParser, [ 61 | ["truth", "Cannot convert truth to a boolean"], 62 | ["lie", "Cannot convert lie to a boolean"], 63 | ["📴", "Cannot convert 📴 to a boolean"], 64 | ["❌", "Cannot convert ❌ to a boolean"], 65 | ["⭕", "Cannot convert ⭕ to a boolean"], 66 | ["✔", "Cannot convert ✔ to a boolean"], 67 | ["✅", "Cannot convert ✅ to a boolean"], 68 | ["🔛", "Cannot convert 🔛 to a boolean"], 69 | ["🆗", "Cannot convert 🆗 to a boolean"], 70 | ]); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/core/tests/parameter/parsers/choice.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { buildChoiceParser } from "../../../src"; 4 | import { testParser, testParserError } from "../parser"; 5 | 6 | describe("choice parser", () => { 7 | describe("(a|b)", () => { 8 | const parser = buildChoiceParser(["a", "b"]); 9 | 10 | testParser(parser, [ 11 | ["a", "a"], 12 | ["b", "b"], 13 | ]); 14 | 15 | testParserError(parser, [ 16 | ["A", "A is not one of (a|b)"], 17 | ["B", "B is not one of (a|b)"], 18 | ["c", "c is not one of (a|b)"], 19 | ]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/tests/parameter/parsers/number.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { numberParser } from "../../../src"; 4 | import { testParser, testParserError } from "../parser"; 5 | 6 | describe("number parser", () => { 7 | testParser(numberParser, [ 8 | ["0", 0], 9 | ["9007199254740991", 9007199254740991], 10 | ["-9007199254740991", -9007199254740991], 11 | ]); 12 | 13 | testParserError(numberParser, [ 14 | ["O", "Cannot convert O to a number"], 15 | ["1trillion", "Cannot convert 1trillion to a number"], 16 | ]); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/core/tests/util/distance.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { expect } from "chai"; 4 | // eslint-disable-next-line no-restricted-imports 5 | import { damerauLevenshtein, type DamerauLevenshteinWeights, filterClosestAlternatives } from "../../src/util/distance"; 6 | 7 | const DEFAULT_WEIGHTS: DamerauLevenshteinWeights = { 8 | insertion: 1, 9 | deletion: 3, 10 | substitution: 2, 11 | transposition: 0, 12 | }; 13 | 14 | type DistanceTestCase = readonly [a: string, b: string, distance: number]; 15 | 16 | function testDistance(a: string, b: string, threshold: number, distance: number) { 17 | it(`distance=${distance}, with threshold=${threshold}`, () => { 18 | const distanceAB = damerauLevenshtein(a, b, { threshold, weights: DEFAULT_WEIGHTS }); 19 | expect(distanceAB).to.equal( 20 | distance, 21 | `Expected ${a} -> ${b} to have distance=${distance} with threshold=${threshold}`, 22 | ); 23 | }); 24 | } 25 | 26 | function testDistances(tests: readonly DistanceTestCase[]) { 27 | for (const test of tests) { 28 | describe(`${test[0]} -> ${test[1]}`, () => { 29 | testDistance(test[0], test[1], Infinity, test[2]); 30 | for (let threshold = 0; threshold <= test[2] + 1; ++threshold) { 31 | const limitedDistance = test[2] > threshold ? Infinity : test[2]; 32 | testDistance(test[0], test[1], threshold, limitedDistance); 33 | } 34 | }); 35 | } 36 | } 37 | 38 | describe("Edit Distance Calculations", () => { 39 | describe("damerauLevenshtein", () => { 40 | testDistances([ 41 | ["foo", "foo", 0], 42 | ["foo", "bar", 3 * DEFAULT_WEIGHTS.substitution], 43 | ["bar", "baz", 1 * DEFAULT_WEIGHTS.substitution], 44 | ["bar", "bra", 1 * DEFAULT_WEIGHTS.transposition], 45 | ["foo", "foobar", 3 * DEFAULT_WEIGHTS.insertion], 46 | ["foobar", "foo", 3 * DEFAULT_WEIGHTS.deletion], 47 | ["dryrun", "dry-run", 1 * DEFAULT_WEIGHTS.insertion], 48 | ["proj", "prepare", 2 * DEFAULT_WEIGHTS.substitution + 3 * DEFAULT_WEIGHTS.insertion], 49 | ["noflag-name", "flagName", 3 * DEFAULT_WEIGHTS.deletion + 1 * DEFAULT_WEIGHTS.substitution], 50 | ]); 51 | }); 52 | 53 | describe("filterClosestAlternatives", () => { 54 | it("sorts alternatives starting with target first", () => { 55 | // GIVEN 56 | const target = "ab"; 57 | const values = ["ax", "abcd", "abab", "xyz"]; 58 | 59 | // WHEN 60 | const alternatives = filterClosestAlternatives(target, values, { threshold: 7, weights: DEFAULT_WEIGHTS }); 61 | 62 | // THEN 63 | expect(alternatives).to.deep.equal(["abab", "abcd", "ax"]); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/core/tests/util/formatting.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { expect } from "chai"; 4 | // eslint-disable-next-line no-restricted-imports 5 | import { formatRowsWithColumns, type JoinGrammar, joinWithGrammar } from "../../src/util/formatting"; 6 | 7 | interface RowsWithColumnsTestCase { 8 | readonly cells: readonly (readonly string[])[]; 9 | readonly separators?: readonly string[]; 10 | readonly result: readonly string[]; 11 | } 12 | 13 | function testRowsWithColumns(tests: readonly RowsWithColumnsTestCase[]) { 14 | describe("formatRowsWithColumns", () => { 15 | for (const test of tests) { 16 | it(`${JSON.stringify(test.cells)} ${JSON.stringify(test.separators)} => ${JSON.stringify(test.result)}`, () => { 17 | const result = formatRowsWithColumns(test.cells, test.separators); 18 | expect(result).to.deep.equal(test.result); 19 | }); 20 | } 21 | }); 22 | } 23 | 24 | testRowsWithColumns([ 25 | { cells: [], separators: [], result: [] }, 26 | { cells: [], result: [] }, 27 | { 28 | cells: [ 29 | ["a", "1"], 30 | ["b", "2"], 31 | ], 32 | result: ["a 1", "b 2"], 33 | }, 34 | { 35 | cells: [ 36 | ["a", "1"], 37 | ["b", "2"], 38 | ], 39 | separators: [" "], 40 | result: ["a 1", "b 2"], 41 | }, 42 | { 43 | cells: [ 44 | ["a", "1"], 45 | ["b", "2"], 46 | ], 47 | separators: ["|"], 48 | result: ["a|1", "b|2"], 49 | }, 50 | { 51 | cells: [ 52 | ["a", "1"], 53 | ["b", "2"], 54 | ], 55 | separators: ["---"], 56 | result: ["a---1", "b---2"], 57 | }, 58 | { 59 | cells: [ 60 | ["aaa", "1"], 61 | ["b", "2"], 62 | ], 63 | result: ["aaa 1", "b 2"], 64 | }, 65 | { cells: [["a", "1"], ["b"]], result: ["a 1", "b"] }, 66 | { cells: [["a", "1", "10", "100"], ["b"]], result: ["a 1 10 100", "b"] }, 67 | { cells: [["a"], []], result: ["a", ""] }, 68 | ]); 69 | 70 | interface JoinTestCase { 71 | readonly parts: readonly string[]; 72 | readonly grammar: JoinGrammar; 73 | readonly result: string; 74 | } 75 | 76 | function testJoins(tests: readonly JoinTestCase[]) { 77 | describe("joinWithGrammar", () => { 78 | for (const test of tests) { 79 | it(`[${test.parts.join()}] ${test.grammar.kind} => ${test.result}`, () => { 80 | const result = joinWithGrammar(test.parts, test.grammar); 81 | expect(result).to.equal(test.result); 82 | }); 83 | } 84 | }); 85 | } 86 | 87 | testJoins([ 88 | { parts: [], grammar: { kind: "conjunctive", conjunction: "and" }, result: "" }, 89 | { parts: ["one"], grammar: { kind: "conjunctive", conjunction: "and" }, result: "one" }, 90 | { parts: ["one", "two"], grammar: { kind: "conjunctive", conjunction: "and" }, result: "one and two" }, 91 | { 92 | parts: ["one", "two", "three"], 93 | grammar: { kind: "conjunctive", conjunction: "and" }, 94 | result: "one, two and three", 95 | }, 96 | { 97 | parts: ["one", "two", "three"], 98 | grammar: { kind: "conjunctive", conjunction: "and", serialComma: true }, 99 | result: "one, two, and three", 100 | }, 101 | ]); 102 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/tsconfig.json", 3 | "compilerOptions": { 4 | // Emit 5 | "emitDeclarationOnly": false, 6 | "noEmit": true, 7 | // Modules 8 | "rootDir": ".", 9 | "moduleResolution": "node", 10 | "types": ["node", "mocha"], 11 | // Interop Constraints 12 | "esModuleInterop": true 13 | }, 14 | "exclude": ["dist/**/*", "lib/**/*"], 15 | "include": ["src/**/*", "tests/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | tests/baselines/local 107 | -------------------------------------------------------------------------------- /packages/create-app/README.md: -------------------------------------------------------------------------------- 1 | # `@stricli/create-app` 2 | 3 | > Generate a new Stricli application 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/@stricli/create-app.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/create-app) 6 | [![NPM Type Definitions](https://img.shields.io/npm/types/@stricli/create-app.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/create-app) 7 | [![NPM Downloads](https://img.shields.io/npm/dm/@stricli/create-app.svg?style=flat-square)](https://www.npmjs.com/package/@stricli/create-app) 8 | 9 | ## Usage 10 | 11 | Run the following command to use this package. 12 | 13 | ``` 14 | npx @stricli/create-app@latest 15 | ``` 16 | 17 | 👉 See **https://bloomberg.github.io/stricli** for documentation on Stricli. 18 | -------------------------------------------------------------------------------- /packages/create-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import common from "../../eslint.config.mjs"; 2 | import ts from "typescript-eslint"; 3 | 4 | export default [ 5 | ...ts.configs.strictTypeChecked, 6 | ...common, 7 | { 8 | languageOptions: { 9 | parserOptions: { 10 | project: ["src/tsconfig.json"], 11 | }, 12 | }, 13 | }, 14 | { 15 | files: ["tests/**/*.ts"], 16 | languageOptions: { 17 | parserOptions: { 18 | project: ["tsconfig.json"], 19 | }, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /packages/create-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stricli/create-app", 3 | "version": "1.1.2", 4 | "description": "Generate a new Stricli application", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bloomberg/stricli/tree/main/packages/create-app" 9 | }, 10 | "author": "Michael Molisani ", 11 | "files": [ 12 | "dist" 13 | ], 14 | "type": "module", 15 | "bin": { 16 | "create-app": "dist/cli.js" 17 | }, 18 | "engines": { 19 | "node": ">=18.x" 20 | }, 21 | "scripts": { 22 | "format": "prettier --config ../../.prettierrc -w .", 23 | "format:check": "prettier --config ../../.prettierrc -c .", 24 | "lint": "eslint src", 25 | "lint:fix": "eslint src tests --fix", 26 | "typecheck": "tsc -p tsconfig.json --noEmit", 27 | "test": "mocha", 28 | "test:clear-baseline": "node scripts/clear_baseline", 29 | "test:accept-baseline": "node scripts/accept_baseline", 30 | "coverage": "c8 npm test", 31 | "build": "tsup", 32 | "prepublishOnly": "npm run build" 33 | }, 34 | "mocha": { 35 | "import": "tsx/esm", 36 | "spec": "tests/**/*.spec.ts" 37 | }, 38 | "c8": { 39 | "reporter": [ 40 | "text", 41 | "lcovonly" 42 | ], 43 | "check-coverage": true, 44 | "skip-full": true 45 | }, 46 | "tsup": { 47 | "entry": [ 48 | "src/bin/cli.ts" 49 | ], 50 | "format": [ 51 | "esm" 52 | ], 53 | "tsconfig": "src/tsconfig.json", 54 | "clean": true, 55 | "splitting": true, 56 | "minify": true 57 | }, 58 | "dependencies": { 59 | "@stricli/auto-complete": "^1.1.2", 60 | "@stricli/core": "^1.1.2" 61 | }, 62 | "devDependencies": { 63 | "@types/chai": "^4.3.16", 64 | "@types/mocha": "^10.0.6", 65 | "@types/node": "^18.19.33", 66 | "@types/sinon": "^17.0.3", 67 | "@typescript-eslint/eslint-plugin": "^8.2.0", 68 | "@typescript-eslint/parser": "^8.2.0", 69 | "c8": "^9.1.0", 70 | "chai": "^5.1.1", 71 | "eslint": "^8.57.0", 72 | "eslint-plugin-prettier": "^5.1.3", 73 | "fs-extra": "^11.2.0", 74 | "memfs": "^4.9.2", 75 | "mocha": "^10.4.0", 76 | "prettier": "^3.2.5", 77 | "sinon": "^18.0.0", 78 | "tsup": "^6.7.0", 79 | "tsx": "^4.8.2", 80 | "type-fest": "^3.5.4", 81 | "typescript": "5.6.x" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/create-app/scripts/accept_baseline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fse from "fs-extra"; 3 | import path from "node:path"; 4 | import url from "node:url"; 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | const baselinesDir = path.join(__dirname, "..", "tests", "baselines"); 7 | fse.copySync(path.join(baselinesDir, "local"), path.join(baselinesDir, "reference"), { recursive: true }); 8 | -------------------------------------------------------------------------------- /packages/create-app/scripts/clear_baseline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fse from "fs-extra"; 3 | import path from "node:path"; 4 | import url from "node:url"; 5 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | const baselinesDir = path.join(__dirname, "..", "tests", "baselines"); 7 | fse.rmSync(path.join(baselinesDir, "local"), { recursive: true }); 8 | fse.rmSync(path.join(baselinesDir, "reference"), { recursive: true }); 9 | -------------------------------------------------------------------------------- /packages/create-app/src/app.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { buildApplication, buildCommand, numberParser } from "@stricli/core"; 4 | import packageJson from "../package.json"; 5 | 6 | const command = buildCommand({ 7 | /* c8 ignore next */ 8 | loader: async () => import("./impl"), 9 | parameters: { 10 | positional: { 11 | kind: "tuple", 12 | parameters: [ 13 | { 14 | brief: "Target path of new package directory", 15 | parse(rawInput) { 16 | return this.path.join(this.process.cwd(), rawInput); 17 | }, 18 | placeholder: "path", 19 | }, 20 | ], 21 | }, 22 | flags: { 23 | type: { 24 | kind: "enum", 25 | brief: "Package type, controls output format of JS files", 26 | values: ["commonjs", "module"], 27 | default: "module", 28 | }, 29 | template: { 30 | kind: "enum", 31 | brief: "Application template to generate", 32 | values: ["single", "multi"], 33 | default: "multi", 34 | }, 35 | autoComplete: { 36 | kind: "boolean", 37 | brief: "Include auto complete postinstall script", 38 | default: true, 39 | }, 40 | name: { 41 | kind: "parsed", 42 | brief: "Package name, if different from directory", 43 | parse: String, 44 | optional: true, 45 | }, 46 | command: { 47 | kind: "parsed", 48 | brief: "Intended bin command, if different from name", 49 | parse: String, 50 | optional: true, 51 | }, 52 | description: { 53 | kind: "parsed", 54 | brief: "Package description", 55 | parse: String, 56 | default: "Stricli command line application", 57 | }, 58 | license: { 59 | kind: "parsed", 60 | brief: "Package license", 61 | parse: String, 62 | default: "MIT", 63 | }, 64 | author: { 65 | kind: "parsed", 66 | brief: "Package author", 67 | parse: String, 68 | default: "", 69 | }, 70 | nodeVersion: { 71 | kind: "parsed", 72 | brief: "Node.js major version to use for engines.node minimum and @types/node, bypasses version discovery logic", 73 | parse: numberParser, 74 | optional: true, 75 | hidden: true, 76 | }, 77 | }, 78 | aliases: { 79 | n: "name", 80 | d: "description", 81 | }, 82 | }, 83 | docs: { 84 | brief: packageJson.description, 85 | }, 86 | }); 87 | 88 | export const app = buildApplication(command, { 89 | name: packageJson.name, 90 | versionInfo: { 91 | currentVersion: packageJson.version, 92 | }, 93 | scanner: { 94 | caseStyle: "allow-kebab-for-camel", 95 | }, 96 | documentation: { 97 | useAliasInUsageLine: true, 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /packages/create-app/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Copyright 2024 Bloomberg Finance L.P. 3 | // Distributed under the terms of the Apache 2.0 license. 4 | import { run } from "@stricli/core"; 5 | import { buildContext } from "../context"; 6 | import { app } from "../app"; 7 | await run(app, process.argv.slice(2), buildContext(process)); 8 | -------------------------------------------------------------------------------- /packages/create-app/src/context.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import type { CommandContext } from "@stricli/core"; 4 | import type { StricliAutoCompleteContext } from "@stricli/auto-complete"; 5 | import fs from "node:fs"; 6 | import os from "node:os"; 7 | import path from "node:path"; 8 | 9 | export interface LocalContext extends CommandContext, StricliAutoCompleteContext { 10 | readonly process: NodeJS.Process; 11 | readonly fs: { 12 | readonly promises: Pick; 13 | }; 14 | readonly path: Pick; 15 | } 16 | 17 | /* c8 ignore start */ 18 | export function buildContext(process: NodeJS.Process): LocalContext { 19 | return { 20 | process, 21 | os, 22 | fs, 23 | path, 24 | }; 25 | } 26 | /* c8 ignore stop */ 27 | -------------------------------------------------------------------------------- /packages/create-app/src/node.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { discoverPackageRegistry, fetchPackageVersions } from "./registry"; 4 | 5 | export interface NodeVersions { 6 | readonly engine: string; 7 | readonly types: string; 8 | } 9 | 10 | const MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION = 22; 11 | 12 | export async function calculateAcceptableNodeVersions(process: NodeJS.Process): Promise { 13 | const majorVersion = Number(process.versions.node.split(".")[0]); 14 | let typesVersion: string | undefined; 15 | 16 | if (majorVersion > MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION) { 17 | // To avoid hitting the registry every time, only run when higher than a statically-known maximum safe value. 18 | const registry = discoverPackageRegistry(process); 19 | const versions = registry && (await fetchPackageVersions(registry, "@types/node")); 20 | if (versions?.includes(process.versions.node)) { 21 | typesVersion = `^${process.versions.node}`; 22 | } else if (versions) { 23 | const typeMajors = new Set(versions.map((version) => Number(version.split(".")[0]))); 24 | if (typeMajors.has(majorVersion)) { 25 | // Previously unknown major version exists, which means MAXIMUM_KNOWN_SAFE_NODE_TYPES_VERSION should be updated. 26 | typesVersion = `${majorVersion}.x`; 27 | } else { 28 | // Filter available major versions to just even (LTS) and pick highest. 29 | // This assumes that types will exist for the LTS version just prior to the current unknown major version. 30 | const highestEvenTypeMajor = [...typeMajors] 31 | .filter((major) => major % 2 === 0) 32 | .toSorted() 33 | .at(-1); 34 | if (highestEvenTypeMajor) { 35 | typesVersion = `${highestEvenTypeMajor}.x`; 36 | process.stderr.write( 37 | `No version of @types/node found with major ${majorVersion}, falling back to ${typesVersion}\n`, 38 | ); 39 | process.stderr.write( 40 | `Rerun this command with the hidden flag --node-version to manually specify the Node.js major version`, 41 | ); 42 | } 43 | } 44 | } 45 | } else { 46 | typesVersion = `${majorVersion}.x`; 47 | } 48 | 49 | if (!typesVersion) { 50 | typesVersion = `${majorVersion}.x`; 51 | // Should only be hit if something went wrong determining registry URL or fetching from registry. 52 | process.stderr.write( 53 | `Unable to determine version of @types/node for ${process.versions.node}, assuming ${typesVersion}\n`, 54 | ); 55 | process.stderr.write( 56 | `Rerun this command with the hidden flag --node-version to manually specify the Node.js major version`, 57 | ); 58 | } 59 | 60 | return { 61 | engine: `>=${majorVersion}`, 62 | types: typesVersion, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/create-app/src/registry.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import child_process from "node:child_process"; 4 | 5 | export function discoverPackageRegistry(process: NodeJS.Process): string | undefined { 6 | if (process.env["NPM_CONFIG_REGISTRY"]) { 7 | return process.env["NPM_CONFIG_REGISTRY"]; 8 | } 9 | 10 | if (process.env["NPM_EXECPATH"]) { 11 | return child_process 12 | .execFileSync(process.execPath, [process.env["NPM_EXECPATH"], "config", "get", "registry"], { 13 | encoding: "utf-8", 14 | }) 15 | .trim(); 16 | } 17 | } 18 | 19 | export async function fetchPackageVersions( 20 | registry: string, 21 | packageName: string, 22 | ): Promise { 23 | const input = registry + (registry.endsWith("/") ? packageName : `/${packageName}`); 24 | const response = await fetch(input); 25 | const data = await response.json(); 26 | if (typeof data === "object" && data && "versions" in data && typeof data.versions === "object") { 27 | return Object.keys(data.versions ?? {}); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/create-app/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "rootDir": "..", 5 | "types": ["node"], 6 | "resolveJsonModule": true, 7 | "target": "esnext", 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "lib": ["esnext"], 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "isolatedModules": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitOverride": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "noUncheckedIndexedAccess": true, 18 | "verbatimModuleSyntax": true 19 | }, 20 | "exclude": [], 21 | "include": ["**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/create-app/tests/baseline.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | import { expect } from "chai"; 4 | import assert from "node:assert"; 5 | import fs from "node:fs"; 6 | import path from "node:path"; 7 | import url from "node:url"; 8 | 9 | const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); 10 | 11 | const baselineDir = path.join(__dirname, "baselines"); 12 | const referenceBaselinesDir = path.join(baselineDir, "reference"); 13 | const localBaselinesDir = path.join(baselineDir, "local"); 14 | 15 | interface FileBaseline { 16 | readonly load: (format: BaselineFormat) => O | undefined; 17 | readonly store: (format: BaselineFormat, result: T) => void; 18 | readonly save: () => void; 19 | } 20 | 21 | export interface BaselineFormat { 22 | readonly serialize: (result: T) => Generator | readonly string[]; 23 | readonly parse: (lines: readonly string[]) => O; 24 | readonly compare: (actual: O, expected: O) => void; 25 | } 26 | 27 | export const StringArrayBaselineFormat: BaselineFormat = { 28 | serialize(result) { 29 | return result; 30 | }, 31 | parse(lines) { 32 | return lines; 33 | }, 34 | compare(actual, expected) { 35 | expect(actual.join("\n")).to.equal(expected.join("\n"), "Help text did not match baseline"); 36 | }, 37 | }; 38 | 39 | const FILE_BASELINES_BY_PATH = new Map(); 40 | 41 | function getFileBaseline(testPath: string): FileBaseline { 42 | let baseline = FILE_BASELINES_BY_PATH.get(testPath); 43 | if (baseline) { 44 | return baseline; 45 | } 46 | let lines: string[] | undefined; 47 | try { 48 | const referenceBaselinesPath = path.join(referenceBaselinesDir, testPath + ".txt"); 49 | lines = fs.readFileSync(referenceBaselinesPath).toString().split(/\n/); 50 | } catch { 51 | // no baseline yet 52 | } 53 | baseline = { 54 | load: (format) => { 55 | if (lines) { 56 | return format.parse(lines); 57 | } 58 | }, 59 | store: (format, result) => { 60 | lines = [...format.serialize(result)]; 61 | }, 62 | save: () => { 63 | if (!lines) { 64 | return; 65 | } 66 | const localBaselinesTestPath = path.join(localBaselinesDir, testPath + ".txt"); 67 | const localBaselinesTestDir = path.dirname(localBaselinesTestPath); 68 | fs.mkdirSync(localBaselinesTestDir, { recursive: true }); 69 | fs.writeFileSync(localBaselinesTestPath, lines.join("\n")); 70 | }, 71 | }; 72 | FILE_BASELINES_BY_PATH.set(testPath, baseline); 73 | return baseline; 74 | } 75 | 76 | after(() => { 77 | for (const baselines of FILE_BASELINES_BY_PATH.values()) { 78 | baselines.save(); 79 | } 80 | }); 81 | 82 | export function compareToBaseline(context: Mocha.Context, format: BaselineFormat, result: T): void { 83 | assert(context.test, "Mocha context does not have a runnable test"); 84 | assert(context.test.file, "Unable to determine file of current test"); 85 | const testPathParts = path.relative(__dirname, context.test.file.replace(/\.spec\.(js|ts)$/, "")).split(path.sep); 86 | testPathParts.push(...context.test.titlePath()); 87 | const baselines = getFileBaseline(testPathParts.join(path.sep)); 88 | 89 | try { 90 | const actualResult = format.parse([...format.serialize(result)]); 91 | const expectedResult = baselines.load(format); 92 | if (!expectedResult) { 93 | throw new Error(`No reference baseline found for test case`); 94 | } 95 | format.compare(actualResult, expectedResult); 96 | } catch (exc) { 97 | baselines.store(format, result); 98 | throw exc; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/create-app/tests/baselines/reference/app/creates new application/checks for @types__node/registry logic/NPM_EXECPATH throws an error.txt: -------------------------------------------------------------------------------- 1 | [STDOUT] 2 | 3 | [STDERR] 4 | Command failed, Error: Failed to execute NPM_EXECPATH 5 | at Context. (#/tests/app.spec.ts:?:?) 6 | 7 | [FILES] -------------------------------------------------------------------------------- /packages/create-app/tests/baselines/reference/app/creates new application/checks for @types__node/registry logic/request to registry throws error.txt: -------------------------------------------------------------------------------- 1 | [STDOUT] 2 | 3 | [STDERR] 4 | Command failed, Error: Failed to fetch data from REGISTRY 5 | at Context. (#/tests/app.spec.ts:?:?) 6 | 7 | [FILES] -------------------------------------------------------------------------------- /packages/create-app/tests/stream.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | export class FakeWritableStream { 4 | #text: string[] = []; 5 | write(str: string): void { 6 | this.#text.push(str); 7 | } 8 | get text(): string { 9 | return this.#text.join(""); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/create-app/tests/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Bloomberg Finance L.P. 2 | // Distributed under the terms of the Apache 2.0 license. 3 | export type DeepPartial = { 4 | [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/create-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/tsconfig.json", 3 | "compilerOptions": { 4 | // Emit 5 | "emitDeclarationOnly": false, 6 | "noEmit": true, 7 | // Modules 8 | "rootDir": ".", 9 | "types": ["node", "mocha"], 10 | "allowImportingTsExtensions": true, 11 | // Interop Constraints 12 | "esModuleInterop": true 13 | }, 14 | "exclude": ["dist/**/*", "lib/**/*"], 15 | "include": ["src/**/*", "tests/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /scripts/add_labels_to_pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | diff_result=$(git diff --name-only origin/main..HEAD) 4 | 5 | if [[ "${diff_result[*]}" =~ packages/core/* ]]; then 6 | echo "Change found in packages/core, adding label for core" 7 | gh pr edit $PR_NUMBER --add-label "core ⚙" 8 | else 9 | echo "No changes found in packages/core, removing label for core" 10 | gh pr edit $PR_NUMBER --remove-label "core ⚙" 11 | fi 12 | 13 | if [[ "${diff_result[*]}" =~ packages/auto-complete/* ]]; then 14 | echo "Change found in packages/auto-complete, adding label for auto-complete" 15 | gh pr edit $PR_NUMBER --add-label "auto-complete 🔮" 16 | else 17 | echo "No changes found in packages/auto-complete, removing label for auto-complete" 18 | gh pr edit $PR_NUMBER --remove-label "auto-complete 🔮" 19 | fi 20 | 21 | if [[ "${diff_result[*]}" =~ packages/create-app/* ]]; then 22 | echo "Change found in packages/create-app, adding label for create-app" 23 | gh pr edit $PR_NUMBER --add-label "create-app 📂" 24 | else 25 | echo "No changes found in packages/create-app, removing label for create-app" 26 | gh pr edit $PR_NUMBER --remove-label "create-app 📂" 27 | fi 28 | 29 | if [[ "${diff_result[*]}" =~ docs/* ]]; then 30 | echo "Change found in docs, adding label for documentation" 31 | gh pr edit $PR_NUMBER --add-label "documentation 📝" 32 | else 33 | echo "No changes found in docs, removing label for documentation" 34 | gh pr edit $PR_NUMBER --remove-label "documentation 📝" 35 | fi 36 | 37 | if [[ "${diff_result[*]}" =~ .github/workflows/* ]]; then 38 | echo "Change found in .github/workflows, adding label for ci" 39 | gh pr edit $PR_NUMBER --add-label "ci 🤖" 40 | else 41 | echo "No changes found in .github/workflows, removing label for ci" 42 | gh pr edit $PR_NUMBER --remove-label "ci 🤖" 43 | fi 44 | --------------------------------------------------------------------------------