├── .commitlintrc.json ├── .git2gus └── config.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── create-github-release.yml │ ├── failureNotifications.yml │ ├── notify-slack-on-pr-open.yml │ ├── onRelease.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .mocharc.json ├── .nycrc ├── .prettierrc.json ├── CHANGELOG.md ├── CONRTIBUTING.md ├── LICENSE ├── README.md ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── eslint.config.js ├── package.json ├── src ├── autocomplete │ ├── bash-spaces.ts │ ├── bash.ts │ ├── powershell.ts │ └── zsh.ts ├── base.ts ├── commands │ └── autocomplete │ │ ├── create.ts │ │ ├── index.ts │ │ └── script.ts ├── hooks │ └── refresh-cache.ts └── index.ts ├── test ├── autocomplete │ ├── bash.test.ts │ ├── powershell.test.ts │ └── zsh.test.ts ├── base.test.ts ├── commands │ └── autocomplete │ │ ├── create.test.ts │ │ ├── index.test.ts │ │ └── script.test.ts ├── helpers │ └── runtest.ts ├── test.oclif.manifest.json ├── tsconfig.json └── zsh_example ├── tsconfig.json └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.git2gus/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "productTag": "a1aB0000000ce2IIAQ", 3 | "defaultBuild": "offcore.tooling.59", 4 | "issueTypeLabels": { 5 | "enhancement": "USER STORY", 6 | "bug": "BUG P3" 7 | }, 8 | "hideWorkItemUrl": true, 9 | "statusWhenClosed": "CLOSED" 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | *.ts text eol=lf 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @oclif/admins 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at heroku-cli@salesforce.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | assignees: '' 6 | --- 7 | 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | Steps to reproduce the behavior: 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment (please complete the following information):** 26 | 27 | - OS & version: [e.g. MacOS Monterey, Ubuntu 20.04.4 LTS, Windows 10] 28 | - Shell/terminal & version [e.g. bash-3.2, bash-5.0, zsh 5.8, powershell 7.2.4, cmd.exe, Windows Terminal w/ powershell, etc... ] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'saturday' 8 | versioning-strategy: 'increase' 9 | labels: 10 | - 'dependencies' 11 | open-pull-requests-limit: 5 12 | pull-request-branch-name: 13 | separator: '-' 14 | commit-message: 15 | # cause a release for non-dev-deps 16 | prefix: fix(deps) 17 | # no release for dev-deps 18 | prefix-development: chore(dev-deps) 19 | ignore: 20 | - dependency-name: '@salesforce/dev-scripts' 21 | - dependency-name: '*' 22 | update-types: ['version-update:semver-major'] 23 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '42 2,5,8,11 * * *' 6 | 7 | jobs: 8 | automerge: 9 | uses: salesforcecli/github-workflows/.github/workflows/automerge.yml@main 10 | secrets: 11 | SVC_CLI_BOT_GITHUB_TOKEN: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} 12 | with: 13 | mergeMethod: squash 14 | -------------------------------------------------------------------------------- /.github/workflows/create-github-release.yml: -------------------------------------------------------------------------------- 1 | name: create-github-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # point at specific branches, or a naming convention via wildcard 8 | - prerelease/** 9 | tags-ignore: 10 | - '*' 11 | workflow_dispatch: 12 | inputs: 13 | prerelease: 14 | type: string 15 | description: 'Name to use for the prerelease: beta, dev, etc. NOTE: If this is already set in the package.json, it does not need to be passed in here.' 16 | 17 | jobs: 18 | release: 19 | # this job will throw if prerelease is true but it doesn't have a prerelease-looking package.json version 20 | uses: salesforcecli/github-workflows/.github/workflows/create-github-release.yml@main 21 | secrets: 22 | SVC_CLI_BOT_GITHUB_TOKEN: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} 23 | with: 24 | prerelease: ${{ inputs.prerelease }} 25 | # If this is a push event, we want to skip the release if there are no semantic commits 26 | # However, if this is a manual release (workflow_dispatch), then we want to disable skip-on-empty 27 | # This helps recover from forgetting to add semantic commits ('fix:', 'feat:', etc.) 28 | skip-on-empty: ${{ github.event_name == 'push' }} 29 | -------------------------------------------------------------------------------- /.github/workflows/failureNotifications.yml: -------------------------------------------------------------------------------- 1 | name: failureNotifications 2 | on: 3 | workflow_run: 4 | workflows: 5 | - create-github-release 6 | - publish 7 | types: 8 | - completed 9 | jobs: 10 | failure-notify: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 13 | steps: 14 | - name: Announce Failure 15 | id: slack 16 | uses: slackapi/slack-github-action@v1.26.0 17 | env: 18 | # for non-CLI-team-owned plugins, you can send this anywhere you like 19 | SLACK_WEBHOOK_URL: ${{ secrets.CLI_ALERTS_SLACK_WEBHOOK }} 20 | SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK 21 | with: 22 | payload: | 23 | { 24 | "text": "${{ github.event.workflow_run.name }} failed: ${{ github.event.workflow_run.repository.name }}", 25 | "blocks": [ 26 | { 27 | "type": "header", 28 | "text": { 29 | "type": "plain_text", 30 | "text": ":bh-alert: ${{ github.event.workflow_run.name }} failed: ${{ github.event.workflow_run.repository.name }} :bh-alert:" 31 | } 32 | }, 33 | { 34 | "type": "section", 35 | "text": { 36 | "type": "mrkdwn", 37 | "text": "Repo: ${{ github.event.workflow_run.repository.html_url }}\nWorkflow name: `${{ github.event.workflow_run.name }}`\nJob url: ${{ github.event.workflow_run.html_url }}" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/notify-slack-on-pr-open.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Slack Notification 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Notify Slack on PR open 12 | env: 13 | WEBHOOK_URL : ${{ secrets.CLI_TEAM_SLACK_WEBHOOK_URL }} 14 | PULL_REQUEST_AUTHOR_ICON_URL : ${{ github.event.pull_request.user.avatar_url }} 15 | PULL_REQUEST_AUTHOR_NAME : ${{ github.event.pull_request.user.login }} 16 | PULL_REQUEST_AUTHOR_PROFILE_URL: ${{ github.event.pull_request.user.html_url }} 17 | PULL_REQUEST_BASE_BRANCH_NAME : ${{ github.event.pull_request.base.ref }} 18 | PULL_REQUEST_COMPARE_BRANCH_NAME : ${{ github.event.pull_request.head.ref }} 19 | PULL_REQUEST_NUMBER : ${{ github.event.pull_request.number }} 20 | PULL_REQUEST_REPO: ${{ github.event.pull_request.head.repo.name }} 21 | PULL_REQUEST_TITLE : ${{ github.event.pull_request.title }} 22 | PULL_REQUEST_URL : ${{ github.event.pull_request.html_url }} 23 | uses: salesforcecli/github-workflows/.github/actions/prNotification@main 24 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | # support manual release in case something goes wrong and needs to be repeated or tested 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: tag that needs to publish 11 | type: string 12 | required: true 13 | jobs: 14 | # parses the package.json version and detects prerelease tag (ex: beta from 4.4.4-beta.0) 15 | getDistTag: 16 | outputs: 17 | tag: ${{ steps.distTag.outputs.tag }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.release.tag_name || inputs.tag }} 23 | - uses: salesforcecli/github-workflows/.github/actions/getPreReleaseTag@main 24 | id: distTag 25 | 26 | npm: 27 | uses: salesforcecli/github-workflows/.github/workflows/npmPublish.yml@main 28 | needs: [getDistTag] 29 | with: 30 | tag: ${{ needs.getDistTag.outputs.tag || 'latest' }} 31 | githubTag: ${{ github.event.release.tag_name || inputs.tag }} 32 | secrets: 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | workflow_dispatch: 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.' 14 | days-before-stale: 30 15 | days-before-close: 7 16 | exempt-issue-labels: 'bug,enhancement,announcement,help wanted,good first issue,waiting for interest,needs response' 17 | # Disable marking PRs as stale 18 | days-before-pr-stale: -1 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | yarn-lockfile-check: 9 | uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main 10 | linux-unit-tests: 11 | needs: yarn-lockfile-check 12 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main 13 | windows-unit-tests: 14 | needs: linux-unit-tests 15 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | node_modules 9 | .idea/ 10 | .vscode/ 11 | 12 | 13 | oclif.manifest.json 14 | npm-shrinkwrap.json 15 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged --concurrent false 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.json": ["prettier --write"], 3 | "*.md": ["prettier --write"], 4 | "+(src|test|bin)/**/*.+(ts|js)": ["eslint --fix", "prettier --write"] 5 | } 6 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register"], 3 | "watch-extensions": ["ts"], 4 | "recursive": true, 5 | "reporter": "spec", 6 | "timeout": 60000, 7 | "node-option": ["loader=ts-node/esm"] 8 | } 9 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".ts" 4 | ], 5 | "include": [ 6 | "src/**/*.ts" 7 | ], 8 | "exclude": [ 9 | "**/*.d.ts" 10 | ], 11 | "all": true 12 | } 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@oclif/prettier-config" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.2.29](https://github.com/oclif/plugin-autocomplete/compare/3.2.28...3.2.29) (2025-05-18) 2 | 3 | ### Bug Fixes 4 | 5 | - **deps:** bump debug from 4.4.0 to 4.4.1 ([#964](https://github.com/oclif/plugin-autocomplete/issues/964)) ([3a2b8d3](https://github.com/oclif/plugin-autocomplete/commit/3a2b8d351f4dc8547aa7fd99a5f311596cad1679)) 6 | 7 | ## [3.2.28](https://github.com/oclif/plugin-autocomplete/compare/3.2.27...3.2.28) (2025-05-04) 8 | 9 | ### Bug Fixes 10 | 11 | - **deps:** bump @oclif/core from 4.2.10 to 4.3.0 ([#955](https://github.com/oclif/plugin-autocomplete/issues/955)) ([ef9e597](https://github.com/oclif/plugin-autocomplete/commit/ef9e59764dc7969a2864a626663e8fe3cf03be06)) 12 | 13 | ## [3.2.27](https://github.com/oclif/plugin-autocomplete/compare/3.2.26...3.2.27) (2025-04-03) 14 | 15 | ### Bug Fixes 16 | 17 | - add missing quote ([e99f169](https://github.com/oclif/plugin-autocomplete/commit/e99f1696b031948e1572b44b131b1e5e91e7970a)) 18 | 19 | ## [3.2.26](https://github.com/oclif/plugin-autocomplete/compare/3.2.25...3.2.26) (2025-03-16) 20 | 21 | ### Bug Fixes 22 | 23 | - **deps:** bump @oclif/core from 4.2.9 to 4.2.10 ([#924](https://github.com/oclif/plugin-autocomplete/issues/924)) ([431b593](https://github.com/oclif/plugin-autocomplete/commit/431b59304436010b0e7188f4f68f403cc2bb7920)) 24 | 25 | ## [3.2.25](https://github.com/oclif/plugin-autocomplete/compare/3.2.24...3.2.25) (2025-03-09) 26 | 27 | ### Bug Fixes 28 | 29 | - **deps:** bump @oclif/core from 4.2.8 to 4.2.9 ([#920](https://github.com/oclif/plugin-autocomplete/issues/920)) ([4488411](https://github.com/oclif/plugin-autocomplete/commit/4488411a0fea6be8cfa8e59eed7c0d17b33d6cdb)) 30 | 31 | ## [3.2.24](https://github.com/oclif/plugin-autocomplete/compare/3.2.23...3.2.24) (2025-02-23) 32 | 33 | ### Bug Fixes 34 | 35 | - **deps:** bump @oclif/core from 4.2.6 to 4.2.8 ([#907](https://github.com/oclif/plugin-autocomplete/issues/907)) ([6a31681](https://github.com/oclif/plugin-autocomplete/commit/6a316814b0074016afdb5a826cad487e9d1ffed6)) 36 | 37 | ## [3.2.23](https://github.com/oclif/plugin-autocomplete/compare/3.2.22...3.2.23) (2025-02-23) 38 | 39 | ### Bug Fixes 40 | 41 | - **deps:** bump ansis from 3.14.0 to 3.16.0 ([#908](https://github.com/oclif/plugin-autocomplete/issues/908)) ([560e05c](https://github.com/oclif/plugin-autocomplete/commit/560e05c8d47363fe8673425c6e3bed20e755baf4)) 42 | 43 | ## [3.2.22](https://github.com/oclif/plugin-autocomplete/compare/3.2.21...3.2.22) (2025-02-16) 44 | 45 | ### Bug Fixes 46 | 47 | - **deps:** bump ansis from 3.10.0 to 3.14.0 ([#903](https://github.com/oclif/plugin-autocomplete/issues/903)) ([9a7ccef](https://github.com/oclif/plugin-autocomplete/commit/9a7ccef172b336737d631a2a557d4389680c4f50)) 48 | 49 | ## [3.2.21](https://github.com/oclif/plugin-autocomplete/compare/3.2.20...3.2.21) (2025-02-10) 50 | 51 | ### Bug Fixes 52 | 53 | - **deps:** bump @oclif/core from 4.2.5 to 4.2.6 ([#895](https://github.com/oclif/plugin-autocomplete/issues/895)) ([ad6f4ab](https://github.com/oclif/plugin-autocomplete/commit/ad6f4ab71b1de816d829e614a3d0c11daf25124d)) 54 | 55 | ## [3.2.20](https://github.com/oclif/plugin-autocomplete/compare/3.2.19...3.2.20) (2025-02-02) 56 | 57 | ### Bug Fixes 58 | 59 | - **deps:** bump ansis from 3.9.0 to 3.10.0 ([#891](https://github.com/oclif/plugin-autocomplete/issues/891)) ([f5e2f39](https://github.com/oclif/plugin-autocomplete/commit/f5e2f390eb36951695634e9b861fbadff541ea61)) 60 | 61 | ## [3.2.19](https://github.com/oclif/plugin-autocomplete/compare/3.2.18...3.2.19) (2025-02-02) 62 | 63 | ### Bug Fixes 64 | 65 | - **deps:** bump @oclif/core from 4.2.4 to 4.2.5 ([#893](https://github.com/oclif/plugin-autocomplete/issues/893)) ([b118e8f](https://github.com/oclif/plugin-autocomplete/commit/b118e8fba44d0ba3906db543de64df4e2286615e)) 66 | 67 | ## [3.2.18](https://github.com/oclif/plugin-autocomplete/compare/3.2.17...3.2.18) (2025-01-19) 68 | 69 | ### Bug Fixes 70 | 71 | - **deps:** bump @oclif/core from 4.2.2 to 4.2.3 ([#879](https://github.com/oclif/plugin-autocomplete/issues/879)) ([6a754b5](https://github.com/oclif/plugin-autocomplete/commit/6a754b5eba7acad8d84e9be228b042e95d0054b0)) 72 | 73 | ## [3.2.17](https://github.com/oclif/plugin-autocomplete/compare/3.2.16...3.2.17) (2025-01-12) 74 | 75 | ### Bug Fixes 76 | 77 | - **deps:** bump @oclif/core from 4.2.0 to 4.2.2 ([#876](https://github.com/oclif/plugin-autocomplete/issues/876)) ([c8dc606](https://github.com/oclif/plugin-autocomplete/commit/c8dc606a43778afe90ee3b33111b433c56f38e54)) 78 | 79 | ## [3.2.16](https://github.com/oclif/plugin-autocomplete/compare/3.2.15...3.2.16) (2024-12-30) 80 | 81 | ### Bug Fixes 82 | 83 | - **deps:** bump ansis from 3.4.0 to 3.5.2 ([#862](https://github.com/oclif/plugin-autocomplete/issues/862)) ([a07a228](https://github.com/oclif/plugin-autocomplete/commit/a07a228007990324f235b12e7f85f034108e00bd)) 84 | 85 | ## [3.2.15](https://github.com/oclif/plugin-autocomplete/compare/3.2.14...3.2.15) (2024-12-22) 86 | 87 | ### Bug Fixes 88 | 89 | - **deps:** bump ansis from 3.3.2 to 3.4.0 ([#856](https://github.com/oclif/plugin-autocomplete/issues/856)) ([558f2d2](https://github.com/oclif/plugin-autocomplete/commit/558f2d2fc2737198e6d511e593d6fed023f20870)) 90 | 91 | ## [3.2.14](https://github.com/oclif/plugin-autocomplete/compare/3.2.13...3.2.14) (2024-12-15) 92 | 93 | ### Bug Fixes 94 | 95 | - **deps:** bump @oclif/core from 4.0.34 to 4.0.37 ([#854](https://github.com/oclif/plugin-autocomplete/issues/854)) ([74d00cf](https://github.com/oclif/plugin-autocomplete/commit/74d00cf30ccbcb38c8e6bcb7db399c953b5de5be)) 96 | 97 | ## [3.2.13](https://github.com/oclif/plugin-autocomplete/compare/3.2.12...3.2.13) (2024-12-08) 98 | 99 | ### Bug Fixes 100 | 101 | - **deps:** bump @oclif/core from 4.0.33 to 4.0.34 ([#848](https://github.com/oclif/plugin-autocomplete/issues/848)) ([dba248e](https://github.com/oclif/plugin-autocomplete/commit/dba248ec0a81a660ab3640e465d7761dca3df49b)) 102 | 103 | ## [3.2.12](https://github.com/oclif/plugin-autocomplete/compare/3.2.11...3.2.12) (2024-12-08) 104 | 105 | ### Bug Fixes 106 | 107 | - **deps:** bump debug from 4.3.7 to 4.4.0 ([#850](https://github.com/oclif/plugin-autocomplete/issues/850)) ([06b7245](https://github.com/oclif/plugin-autocomplete/commit/06b7245febce38fb614b5ed2aed66f3a61b9a9e0)) 108 | 109 | ## [3.2.11](https://github.com/oclif/plugin-autocomplete/compare/3.2.10...3.2.11) (2024-11-25) 110 | 111 | ### Bug Fixes 112 | 113 | - **deps:** bump @oclif/core from 4.0.32 to 4.0.33 ([#835](https://github.com/oclif/plugin-autocomplete/issues/835)) ([68ebc7d](https://github.com/oclif/plugin-autocomplete/commit/68ebc7d1998876f87282c9a2c3eb3584cc6d8a03)) 114 | 115 | ## [3.2.10](https://github.com/oclif/plugin-autocomplete/compare/3.2.9...3.2.10) (2024-11-17) 116 | 117 | ### Bug Fixes 118 | 119 | - **deps:** bump cross-spawn from 7.0.3 to 7.0.5 ([#832](https://github.com/oclif/plugin-autocomplete/issues/832)) ([6ba57fb](https://github.com/oclif/plugin-autocomplete/commit/6ba57fbff9d1b32072d367837fc58cee968a4c1d)) 120 | 121 | ## [3.2.9](https://github.com/oclif/plugin-autocomplete/compare/3.2.8...3.2.9) (2024-11-17) 122 | 123 | ### Bug Fixes 124 | 125 | - **deps:** bump @oclif/core from 4.0.31 to 4.0.32 ([#831](https://github.com/oclif/plugin-autocomplete/issues/831)) ([f71fae0](https://github.com/oclif/plugin-autocomplete/commit/f71fae00a1d244e479b7966d7f14d72cf6f17272)) 126 | 127 | ## [3.2.8](https://github.com/oclif/plugin-autocomplete/compare/3.2.7...3.2.8) (2024-11-03) 128 | 129 | ### Bug Fixes 130 | 131 | - **deps:** bump @oclif/core from 4.0.30 to 4.0.31 ([#823](https://github.com/oclif/plugin-autocomplete/issues/823)) ([037c7ec](https://github.com/oclif/plugin-autocomplete/commit/037c7ec8ee62e60430a86706ab387b6501282848)) 132 | 133 | ## [3.2.7](https://github.com/oclif/plugin-autocomplete/compare/3.2.6...3.2.7) (2024-10-27) 134 | 135 | ### Bug Fixes 136 | 137 | - **deps:** bump @oclif/core from 4.0.29 to 4.0.30 ([#819](https://github.com/oclif/plugin-autocomplete/issues/819)) ([71a4210](https://github.com/oclif/plugin-autocomplete/commit/71a4210137bb6f272a91d712718ce8c7fa7a0e15)) 138 | 139 | ## [3.2.6](https://github.com/oclif/plugin-autocomplete/compare/3.2.5...3.2.6) (2024-10-13) 140 | 141 | ### Bug Fixes 142 | 143 | - **deps:** bump @oclif/core from 4.0.27 to 4.0.28 ([#809](https://github.com/oclif/plugin-autocomplete/issues/809)) ([8f13ae0](https://github.com/oclif/plugin-autocomplete/commit/8f13ae0324b09c68e2aacd40a79725d7bc7841b8)) 144 | 145 | ## [3.2.5](https://github.com/oclif/plugin-autocomplete/compare/3.2.4...3.2.5) (2024-09-29) 146 | 147 | ### Bug Fixes 148 | 149 | - **deps:** bump @oclif/core from 4.0.22 to 4.0.23 ([#802](https://github.com/oclif/plugin-autocomplete/issues/802)) ([a7e7865](https://github.com/oclif/plugin-autocomplete/commit/a7e786585317232f45fcd45c65d908cb5494a25d)) 150 | 151 | ## [3.2.4](https://github.com/oclif/plugin-autocomplete/compare/3.2.3...3.2.4) (2024-09-15) 152 | 153 | ### Bug Fixes 154 | 155 | - **deps:** bump @oclif/core from 4.0.21 to 4.0.22 ([#794](https://github.com/oclif/plugin-autocomplete/issues/794)) ([0f0293a](https://github.com/oclif/plugin-autocomplete/commit/0f0293a9be1266350ea63a18d027b14ac8461219)) 156 | 157 | ## [3.2.3](https://github.com/oclif/plugin-autocomplete/compare/3.2.2...3.2.3) (2024-09-08) 158 | 159 | ### Bug Fixes 160 | 161 | - **deps:** bump @oclif/core from 4.0.19 to 4.0.21 ([#788](https://github.com/oclif/plugin-autocomplete/issues/788)) ([bfc8bfd](https://github.com/oclif/plugin-autocomplete/commit/bfc8bfd75c75e48c9966bb460c53aa80a4f075ec)) 162 | 163 | ## [3.2.2](https://github.com/oclif/plugin-autocomplete/compare/3.2.1...3.2.2) (2024-08-25) 164 | 165 | ### Bug Fixes 166 | 167 | - **deps:** bump @oclif/core from 4.0.17 to 4.0.19 ([#774](https://github.com/oclif/plugin-autocomplete/issues/774)) ([c95e3fa](https://github.com/oclif/plugin-autocomplete/commit/c95e3faf256825c335fe324258c82dd7d151e395)) 168 | 169 | ## [3.2.1](https://github.com/oclif/plugin-autocomplete/compare/3.2.0...3.2.1) (2024-08-25) 170 | 171 | ### Bug Fixes 172 | 173 | - **deps:** bump micromatch from 4.0.7 to 4.0.8 ([#776](https://github.com/oclif/plugin-autocomplete/issues/776)) ([58f59b4](https://github.com/oclif/plugin-autocomplete/commit/58f59b456c643e91d67c4e8ca77b1ead51185df9)) 174 | 175 | # [3.2.0](https://github.com/oclif/plugin-autocomplete/compare/3.1.11...3.2.0) (2024-08-01) 176 | 177 | ### Features 178 | 179 | - add `refresh-cache` hook ([cbda936](https://github.com/oclif/plugin-autocomplete/commit/cbda9361ac1a0f0d47110331145eb022b48a2d9b)) 180 | 181 | ## [3.1.11](https://github.com/oclif/plugin-autocomplete/compare/3.1.10...3.1.11) (2024-07-28) 182 | 183 | ### Bug Fixes 184 | 185 | - **deps:** bump @oclif/core from 4.0.12 to 4.0.17 ([#752](https://github.com/oclif/plugin-autocomplete/issues/752)) ([f6fd2d1](https://github.com/oclif/plugin-autocomplete/commit/f6fd2d1fa7a98ce290641e76683f39ed871ec116)) 186 | 187 | ## [3.1.10](https://github.com/oclif/plugin-autocomplete/compare/3.1.9...3.1.10) (2024-07-28) 188 | 189 | ### Bug Fixes 190 | 191 | - **deps:** bump debug from 4.3.5 to 4.3.6 ([#749](https://github.com/oclif/plugin-autocomplete/issues/749)) ([deeb089](https://github.com/oclif/plugin-autocomplete/commit/deeb089f22936394fe873c8656bbb967c693cc80)) 192 | 193 | ## [3.1.9](https://github.com/oclif/plugin-autocomplete/compare/3.1.8...3.1.9) (2024-07-22) 194 | 195 | ### Bug Fixes 196 | 197 | - husky 9.1.1 fix ([e44652b](https://github.com/oclif/plugin-autocomplete/commit/e44652b148fb16c849cfdb7513c01fa56cf0401c)) 198 | 199 | ## [3.1.8](https://github.com/oclif/plugin-autocomplete/compare/3.1.7...3.1.8) (2024-07-21) 200 | 201 | ### Bug Fixes 202 | 203 | - **deps:** bump ansis from 3.2.1 to 3.3.1 ([#742](https://github.com/oclif/plugin-autocomplete/issues/742)) ([8550fe4](https://github.com/oclif/plugin-autocomplete/commit/8550fe4fb9d6d4f844a296304585364b52ac5eb8)) 204 | 205 | ## [3.1.7](https://github.com/oclif/plugin-autocomplete/compare/3.1.6...3.1.7) (2024-07-14) 206 | 207 | ### Bug Fixes 208 | 209 | - **deps:** bump @oclif/core from 4.0.8 to 4.0.12 ([#739](https://github.com/oclif/plugin-autocomplete/issues/739)) ([538499d](https://github.com/oclif/plugin-autocomplete/commit/538499dedea9c763755db5cb944546e1001df5e9)) 210 | 211 | ## [3.1.6](https://github.com/oclif/plugin-autocomplete/compare/3.1.5...3.1.6) (2024-07-08) 212 | 213 | ### Bug Fixes 214 | 215 | - **deps:** bump @oclif/core from 4.0.7 to 4.0.8 ([#730](https://github.com/oclif/plugin-autocomplete/issues/730)) ([91b59f8](https://github.com/oclif/plugin-autocomplete/commit/91b59f875ed99d28e3139acefc3b8669b7fb5419)) 216 | 217 | ## [3.1.5](https://github.com/oclif/plugin-autocomplete/compare/3.1.4...3.1.5) (2024-06-30) 218 | 219 | ### Bug Fixes 220 | 221 | - **deps:** bump @oclif/core from 4.0.6 to 4.0.7 ([#726](https://github.com/oclif/plugin-autocomplete/issues/726)) ([36d99a0](https://github.com/oclif/plugin-autocomplete/commit/36d99a053667987592af190a6d5c7c5b441adcf8)) 222 | 223 | ## [3.1.4](https://github.com/oclif/plugin-autocomplete/compare/3.1.3...3.1.4) (2024-06-16) 224 | 225 | ### Bug Fixes 226 | 227 | - **deps:** bump @oclif/core from 4.0.0 to 4.0.6 ([#713](https://github.com/oclif/plugin-autocomplete/issues/713)) ([2e3c294](https://github.com/oclif/plugin-autocomplete/commit/2e3c294f48ba04d0f31fbe7b44df1e8a85527906)) 228 | 229 | ## [3.1.3](https://github.com/oclif/plugin-autocomplete/compare/3.1.2...3.1.3) (2024-06-13) 230 | 231 | ### Bug Fixes 232 | 233 | - **deps:** bump braces from 3.0.2 to 3.0.3 ([#711](https://github.com/oclif/plugin-autocomplete/issues/711)) ([62169a1](https://github.com/oclif/plugin-autocomplete/commit/62169a1e39a4b8b7c041d50cde0858f6cc3b24c9)) 234 | 235 | ## [3.1.2](https://github.com/oclif/plugin-autocomplete/compare/3.1.1...3.1.2) (2024-06-04) 236 | 237 | ### Bug Fixes 238 | 239 | - core v4 ([78be9f0](https://github.com/oclif/plugin-autocomplete/commit/78be9f08a070c30e1edd873bf40730c41e3f70be)) 240 | 241 | ## [3.1.1](https://github.com/oclif/plugin-autocomplete/compare/3.1.0...3.1.1) (2024-06-02) 242 | 243 | ### Bug Fixes 244 | 245 | - **deps:** bump debug from 4.3.4 to 4.3.5 ([#702](https://github.com/oclif/plugin-autocomplete/issues/702)) ([bd11d78](https://github.com/oclif/plugin-autocomplete/commit/bd11d78f16d3be4b216bbf97437705b233141483)) 246 | 247 | # [3.1.0](https://github.com/oclif/plugin-autocomplete/compare/3.0.18...3.1.0) (2024-05-31) 248 | 249 | ### Features 250 | 251 | - use @oclif/core v4 ([b14f4a9](https://github.com/oclif/plugin-autocomplete/commit/b14f4a953bfb7fd9a018790ab87d74cd2a7dfc8a)) 252 | - use ansis instead of chalk ([22d3226](https://github.com/oclif/plugin-autocomplete/commit/22d32265c91ae70195f82ec80bba08f8c73bf115)) 253 | 254 | ## [3.0.18](https://github.com/oclif/plugin-autocomplete/compare/3.0.17...3.0.18) (2024-05-12) 255 | 256 | ### Bug Fixes 257 | 258 | - **deps:** bump @oclif/core from 3.26.5 to 3.26.6 ([#687](https://github.com/oclif/plugin-autocomplete/issues/687)) ([8edb1fc](https://github.com/oclif/plugin-autocomplete/commit/8edb1fc0bbf8069cf28f9587baf8a44cb2816423)) 259 | 260 | ## [3.0.17](https://github.com/oclif/plugin-autocomplete/compare/3.0.16...3.0.17) (2024-05-05) 261 | 262 | ### Bug Fixes 263 | 264 | - **deps:** bump @oclif/core from 3.26.4 to 3.26.5 ([#683](https://github.com/oclif/plugin-autocomplete/issues/683)) ([59e111c](https://github.com/oclif/plugin-autocomplete/commit/59e111ca38f1f674c6ebfaa73d6096375e1f3511)) 265 | 266 | ## [3.0.16](https://github.com/oclif/plugin-autocomplete/compare/3.0.15...3.0.16) (2024-04-21) 267 | 268 | ### Bug Fixes 269 | 270 | - **deps:** bump @oclif/core from 3.26.3 to 3.26.4 ([#671](https://github.com/oclif/plugin-autocomplete/issues/671)) ([f283e8f](https://github.com/oclif/plugin-autocomplete/commit/f283e8faafc7bb61555dd9fa90e4778319587472)) 271 | 272 | ## [3.0.15](https://github.com/oclif/plugin-autocomplete/compare/3.0.14...3.0.15) (2024-04-14) 273 | 274 | ### Bug Fixes 275 | 276 | - **deps:** bump @oclif/core from 3.26.0 to 3.26.3 ([#667](https://github.com/oclif/plugin-autocomplete/issues/667)) ([014b76c](https://github.com/oclif/plugin-autocomplete/commit/014b76ca13730ce686f13aa1644921bcd675a8bb)) 277 | 278 | ## [3.0.14](https://github.com/oclif/plugin-autocomplete/compare/3.0.13...3.0.14) (2024-04-14) 279 | 280 | ### Bug Fixes 281 | 282 | - **deps:** bump ejs from 3.1.9 to 3.1.10 ([#668](https://github.com/oclif/plugin-autocomplete/issues/668)) ([8dc03d1](https://github.com/oclif/plugin-autocomplete/commit/8dc03d1ae51081ff783170e75bcf8a8b28950bf2)) 283 | 284 | ## [3.0.13](https://github.com/oclif/plugin-autocomplete/compare/3.0.12...3.0.13) (2024-03-10) 285 | 286 | ### Bug Fixes 287 | 288 | - **deps:** bump @oclif/core from 3.19.6 to 3.23.0 ([#638](https://github.com/oclif/plugin-autocomplete/issues/638)) ([53cf783](https://github.com/oclif/plugin-autocomplete/commit/53cf783c1893935d790a6639b24e208af803134a)) 289 | 290 | ## [3.0.12](https://github.com/oclif/plugin-autocomplete/compare/3.0.11...3.0.12) (2024-03-06) 291 | 292 | ### Bug Fixes 293 | 294 | - publish npm-shrinkwrap.json ([fe7947a](https://github.com/oclif/plugin-autocomplete/commit/fe7947a39edd900aca16b11618bce1aa0588bbcf)) 295 | 296 | ## [3.0.11](https://github.com/oclif/plugin-autocomplete/compare/3.0.10...3.0.11) (2024-02-26) 297 | 298 | ### Bug Fixes 299 | 300 | - output correct CLI binary name in 'autocomplete' command ([77206a5](https://github.com/oclif/plugin-autocomplete/commit/77206a53464e825edbd2b68554df09cd3daad02f)) 301 | 302 | ## [3.0.10](https://github.com/oclif/plugin-autocomplete/compare/3.0.9...3.0.10) (2024-02-22) 303 | 304 | ### Bug Fixes 305 | 306 | - **deps:** bump ip from 1.1.5 to 1.1.9 ([#626](https://github.com/oclif/plugin-autocomplete/issues/626)) ([88f3a58](https://github.com/oclif/plugin-autocomplete/commit/88f3a5824a96b74d20a898e3b9146e0cac18a71e)) 307 | 308 | ## [3.0.9](https://github.com/oclif/plugin-autocomplete/compare/3.0.8...3.0.9) (2024-02-18) 309 | 310 | ### Bug Fixes 311 | 312 | - **deps:** bump @oclif/core from 3.19.1 to 3.19.2 ([#623](https://github.com/oclif/plugin-autocomplete/issues/623)) ([b2e58a1](https://github.com/oclif/plugin-autocomplete/commit/b2e58a16091941761eb509a91f6c1ac4440c3b12)) 313 | 314 | ## [3.0.8](https://github.com/oclif/plugin-autocomplete/compare/3.0.7...3.0.8) (2024-02-11) 315 | 316 | ### Bug Fixes 317 | 318 | - **deps:** bump @oclif/core from 3.18.2 to 3.19.1 ([#619](https://github.com/oclif/plugin-autocomplete/issues/619)) ([246c150](https://github.com/oclif/plugin-autocomplete/commit/246c150c7f7f6fe90b81e32f139bd7572c3eb824)) 319 | 320 | ## [3.0.7](https://github.com/oclif/plugin-autocomplete/compare/3.0.6...3.0.7) (2024-02-04) 321 | 322 | ### Bug Fixes 323 | 324 | - **deps:** bump @oclif/core from 3.18.1 to 3.18.2 ([#611](https://github.com/oclif/plugin-autocomplete/issues/611)) ([62ce618](https://github.com/oclif/plugin-autocomplete/commit/62ce618b256aa2bf65af31cf732c981853a6b921)) 325 | 326 | ## [3.0.6](https://github.com/oclif/plugin-autocomplete/compare/3.0.5...3.0.6) (2024-01-31) 327 | 328 | ### Bug Fixes 329 | 330 | - update help text ([#608](https://github.com/oclif/plugin-autocomplete/issues/608)) ([8975f10](https://github.com/oclif/plugin-autocomplete/commit/8975f102079f71eb888d4f3ccfe3a1e2a0df3d40)) 331 | 332 | ## [3.0.5](https://github.com/oclif/plugin-autocomplete/compare/3.0.4...3.0.5) (2024-01-07) 333 | 334 | ### Bug Fixes 335 | 336 | - **deps:** bump @oclif/core from 3.15.1 to 3.16.0 ([#588](https://github.com/oclif/plugin-autocomplete/issues/588)) ([3eb26c1](https://github.com/oclif/plugin-autocomplete/commit/3eb26c19160a112b2e0b7fce4870878ef653a509)) 337 | 338 | ## [3.0.4](https://github.com/oclif/plugin-autocomplete/compare/3.0.3...3.0.4) (2023-12-25) 339 | 340 | ### Bug Fixes 341 | 342 | - **deps:** bump @oclif/core from 3.15.0 to 3.15.1 ([#575](https://github.com/oclif/plugin-autocomplete/issues/575)) ([727169a](https://github.com/oclif/plugin-autocomplete/commit/727169ac08d593b0296a9f9954efca47cf3ef750)) 343 | 344 | ## [3.0.3](https://github.com/oclif/plugin-autocomplete/compare/3.0.2...3.0.3) (2023-12-10) 345 | 346 | ### Bug Fixes 347 | 348 | - **deps:** bump @oclif/core from 3.12.0 to 3.14.1 ([#566](https://github.com/oclif/plugin-autocomplete/issues/566)) ([8a44057](https://github.com/oclif/plugin-autocomplete/commit/8a4405769bee1e0977787de5638d09eda005fd2e)) 349 | 350 | ## [3.0.2](https://github.com/oclif/plugin-autocomplete/compare/3.0.1...3.0.2) (2023-11-21) 351 | 352 | ### Bug Fixes 353 | 354 | - bump oclif/core ([26c5395](https://github.com/oclif/plugin-autocomplete/commit/26c5395ae02df25f45c7eeb45a3c60b3aee46ae3)) 355 | 356 | ## [3.0.1](https://github.com/oclif/plugin-autocomplete/compare/2.3.11-dev.1...3.0.1) (2023-10-30) 357 | 358 | ### Bug Fixes 359 | 360 | - 3.0.0 release ([34a55e0](https://github.com/oclif/plugin-autocomplete/commit/34a55e00d539af3fae8daef12f46ae31ce012de7)) 361 | 362 | ## [2.3.11-dev.1](https://github.com/oclif/plugin-autocomplete/compare/2.3.11-dev.0...2.3.11-dev.1) (2023-10-27) 363 | 364 | ## [2.3.11-dev.0](https://github.com/oclif/plugin-autocomplete/compare/2.3.10...2.3.11-dev.0) (2023-10-24) 365 | 366 | ## [2.3.10](https://github.com/oclif/plugin-autocomplete/compare/2.3.9...2.3.10) (2023-10-17) 367 | 368 | ### Bug Fixes 369 | 370 | - **deps:** bump @babel/traverse from 7.10.3 to 7.23.2 ([07ee85b](https://github.com/oclif/plugin-autocomplete/commit/07ee85bc3c5bb4673d2eb539d16f85c85910267f)) 371 | 372 | ## [2.3.9](https://github.com/oclif/plugin-autocomplete/compare/2.3.8...2.3.9) (2023-09-28) 373 | 374 | ### Bug Fixes 375 | 376 | - **deps:** bump get-func-name from 2.0.0 to 2.0.2 ([762f264](https://github.com/oclif/plugin-autocomplete/commit/762f26444a9d14cd434a1704c0d6e02b25e39ada)) 377 | 378 | ## [2.3.8](https://github.com/oclif/plugin-autocomplete/compare/2.3.7...2.3.8) (2023-09-05) 379 | 380 | ### Bug Fixes 381 | 382 | - remove fs-extra ([7ad7356](https://github.com/oclif/plugin-autocomplete/commit/7ad7356fe80bdf3ed572a11fc34cb356f3a1c943)) 383 | 384 | ## [2.3.7](https://github.com/oclif/plugin-autocomplete/compare/2.3.6...2.3.7) (2023-09-03) 385 | 386 | ### Bug Fixes 387 | 388 | - **deps:** bump @oclif/core from 2.11.10 to 2.15.0 ([e091fec](https://github.com/oclif/plugin-autocomplete/commit/e091fec15bb052335da688611be35b58f72506f4)) 389 | 390 | ## [2.3.6](https://github.com/oclif/plugin-autocomplete/compare/2.3.5...2.3.6) (2023-08-07) 391 | 392 | ### Bug Fixes 393 | 394 | - **deps:** bump ansi-regex from 3.0.0 to 3.0.1 ([2a8c9b4](https://github.com/oclif/plugin-autocomplete/commit/2a8c9b482acd6907e2817572fc0f21c2293b3113)) 395 | 396 | ## [2.3.5](https://github.com/oclif/plugin-autocomplete/compare/2.3.4...2.3.5) (2023-08-06) 397 | 398 | ### Bug Fixes 399 | 400 | - **deps:** bump @oclif/core from 2.11.1 to 2.11.7 ([206dc42](https://github.com/oclif/plugin-autocomplete/commit/206dc422a7103402992900d0b7eeb6d14dee8b34)) 401 | 402 | ## [2.3.4](https://github.com/oclif/plugin-autocomplete/compare/2.3.3...2.3.4) (2023-08-02) 403 | 404 | ### Bug Fixes 405 | 406 | - block pwsh completion if CLI uses colon sep ([a15b628](https://github.com/oclif/plugin-autocomplete/commit/a15b628aa0013a7c780ad9bd79fdfd12d1e5bdab)) 407 | 408 | ## [2.3.3](https://github.com/oclif/plugin-autocomplete/compare/2.3.2...2.3.3) (2023-07-19) 409 | 410 | ### Bug Fixes 411 | 412 | - **deps:** bump word-wrap from 1.2.3 to 1.2.4 ([9814e11](https://github.com/oclif/plugin-autocomplete/commit/9814e11ca579a35dac8ee4013e230b46e5e94228)) 413 | 414 | ## [2.3.2](https://github.com/oclif/plugin-autocomplete/compare/2.3.1...2.3.2) (2023-07-11) 415 | 416 | ### Bug Fixes 417 | 418 | - **deps:** bump semver from 5.7.1 to 5.7.2 ([76a4c77](https://github.com/oclif/plugin-autocomplete/commit/76a4c77d5ce3d43bdfeee767955aa1dc8006276c)) 419 | 420 | ## [2.3.1](https://github.com/oclif/plugin-autocomplete/compare/2.3.0...2.3.1) (2023-06-26) 421 | 422 | ### Bug Fixes 423 | 424 | - render ejs templates ([b4d2af8](https://github.com/oclif/plugin-autocomplete/commit/b4d2af8149afa22b212e131c0dce4adfd375e271)) 425 | 426 | # [2.3.0](https://github.com/oclif/plugin-autocomplete/compare/2.2.0...2.3.0) (2023-05-30) 427 | 428 | ### Features 429 | 430 | - support bin aliases ([#470](https://github.com/oclif/plugin-autocomplete/issues/470)) ([9eb97a7](https://github.com/oclif/plugin-autocomplete/commit/9eb97a7bc514899ae99c7cd81f6782e50bbff757)) 431 | 432 | # [2.2.0](https://github.com/oclif/plugin-autocomplete/compare/2.1.9...2.2.0) (2023-05-09) 433 | 434 | ### Features 435 | 436 | - powershell autocomplete ([#441](https://github.com/oclif/plugin-autocomplete/issues/441)) ([53697c4](https://github.com/oclif/plugin-autocomplete/commit/53697c42c8c4fd1fe6ac3f4cd2f91efd5ee399cd)) 437 | 438 | ## [2.1.9](https://github.com/oclif/plugin-autocomplete/compare/2.1.8...2.1.9) (2023-04-16) 439 | 440 | ### Bug Fixes 441 | 442 | - **deps:** bump @oclif/core from 2.8.0 to 2.8.2 ([6c0cb44](https://github.com/oclif/plugin-autocomplete/commit/6c0cb445720d0e31b460614f5d8278590ded550d)) 443 | 444 | ## [2.1.8](https://github.com/oclif/plugin-autocomplete/compare/2.1.7...2.1.8) (2023-04-02) 445 | 446 | ### Bug Fixes 447 | 448 | - **deps:** bump @oclif/core from 2.7.1 to 2.8.0 ([2b79073](https://github.com/oclif/plugin-autocomplete/commit/2b790736cceeb02c9edfb1dac9dc2c2fa303c85e)) 449 | 450 | ## [2.1.7](https://github.com/oclif/plugin-autocomplete/compare/2.1.6...2.1.7) (2023-03-26) 451 | 452 | ### Bug Fixes 453 | 454 | - **deps:** bump @oclif/core from 2.6.4 to 2.7.1 ([d04b7ef](https://github.com/oclif/plugin-autocomplete/commit/d04b7ef0af08b4547b02e4afa32c64ace4499e70)) 455 | 456 | ## [2.1.6](https://github.com/oclif/plugin-autocomplete/compare/2.1.5...2.1.6) (2023-03-19) 457 | 458 | ### Bug Fixes 459 | 460 | - **deps:** bump @oclif/core from 2.6.2 to 2.6.4 ([ead2102](https://github.com/oclif/plugin-autocomplete/commit/ead2102ba44df28dfe9fa8e2c68fc93d9be80f5b)) 461 | 462 | ## [2.1.5](https://github.com/oclif/plugin-autocomplete/compare/2.1.4...2.1.5) (2023-03-12) 463 | 464 | ### Bug Fixes 465 | 466 | - **deps:** bump @oclif/core from 2.4.0 to 2.6.2 ([a4ea13f](https://github.com/oclif/plugin-autocomplete/commit/a4ea13f7a78159bccf09b368d26146bda2ef983c)) 467 | 468 | ## [2.1.4](https://github.com/oclif/plugin-autocomplete/compare/2.1.3...2.1.4) (2023-03-05) 469 | 470 | ### Bug Fixes 471 | 472 | - **deps:** bump @oclif/core from 2.3.1 to 2.4.0 ([a08df91](https://github.com/oclif/plugin-autocomplete/commit/a08df91533be044f7f3fde2fc782202478275b3d)) 473 | 474 | ## [2.1.3](https://github.com/oclif/plugin-autocomplete/compare/2.1.2...2.1.3) (2023-02-26) 475 | 476 | ### Bug Fixes 477 | 478 | - **deps:** bump @oclif/core from 2.1.7 to 2.3.1 ([0c80ba2](https://github.com/oclif/plugin-autocomplete/commit/0c80ba23fbe9a9a137dc211629f493377f6e7d0a)) 479 | 480 | ## [2.1.2](https://github.com/oclif/plugin-autocomplete/compare/2.1.1...2.1.2) (2023-02-22) 481 | 482 | ### Bug Fixes 483 | 484 | - replace all dashes in autocomplete env var ([#427](https://github.com/oclif/plugin-autocomplete/issues/427)) ([ac82017](https://github.com/oclif/plugin-autocomplete/commit/ac820171c1ddf12f33aa81c1c38fa3a8f24cc78e)) 485 | 486 | ## [2.1.1](https://github.com/oclif/plugin-autocomplete/compare/2.1.0...2.1.1) (2023-02-19) 487 | 488 | ### Bug Fixes 489 | 490 | - **deps:** bump @oclif/core from 2.1.2 to 2.1.7 ([2158f5d](https://github.com/oclif/plugin-autocomplete/commit/2158f5dcecfa15c60642ac6ba09d2282c9a83347)) 491 | 492 | # [2.1.0](https://github.com/oclif/plugin-autocomplete/compare/1.4.6...2.1.0) (2023-02-15) 493 | 494 | ### Bug Fixes 495 | 496 | - **aliases:** add generic topic description ([b00353f](https://github.com/oclif/plugin-autocomplete/commit/b00353f179373661538d9de34bfe796058c31477)) 497 | - allow to switch between comp implementation ([e9b42aa](https://github.com/oclif/plugin-autocomplete/commit/e9b42aa033f2bb0aa6d5841949c95436aafa4528)) 498 | - check config.topicSeparator prop ([4890d4f](https://github.com/oclif/plugin-autocomplete/commit/4890d4fa11e0effcb832a1fb6df1f96a5a14ec69)) 499 | - complete files by default ([438396f](https://github.com/oclif/plugin-autocomplete/commit/438396f817023a389b77bcade0949f2ecc17dc4e)) 500 | - cotopic func missing flags current state ([18cd38d](https://github.com/oclif/plugin-autocomplete/commit/18cd38d90446f94a2d0d053171fba8a1db396798)) 501 | - don't gen flag case block if cmd=cotopic ([895e5d3](https://github.com/oclif/plugin-autocomplete/commit/895e5d35d48952cc611734ceb13c4f7eb4cb5fe0)) 502 | - generate flag comp for top-level commands ([6ec795e](https://github.com/oclif/plugin-autocomplete/commit/6ec795ed47eb201a4f2a3641da108b4ba4e3b459)) 503 | - generate topics for aliases ([ca03e17](https://github.com/oclif/plugin-autocomplete/commit/ca03e1761d07cc2dde4e57f7c1a986c15f6e18e1)) 504 | - handle topics without desc ([9a6b4fb](https://github.com/oclif/plugin-autocomplete/commit/9a6b4fb0572f72e9f1f7c92972186208c930465b)) 505 | - inline/skip flag comp for top-level args ([a1412d6](https://github.com/oclif/plugin-autocomplete/commit/a1412d68b2bbe4e384c61f5836a2d863765a5608)) 506 | - lint fix ([38f3061](https://github.com/oclif/plugin-autocomplete/commit/38f3061e7b236a116cf6888ca778cf9cb8e49e66)) 507 | - make arg spec to set `words/CURRENT` ([e260069](https://github.com/oclif/plugin-autocomplete/commit/e2600692788b6a9506b59e00e2d24c968a9afd5d)) 508 | - make short/long exclusives ([bb5596a](https://github.com/oclif/plugin-autocomplete/commit/bb5596af192855b488f80ee5e3f53a7f71e4f722)) 509 | - oclif v2 changes ([14b4e38](https://github.com/oclif/plugin-autocomplete/commit/14b4e389a616a3c9491a7191a090a9fe2191d320)) 510 | - prefer summary over description ([a83b003](https://github.com/oclif/plugin-autocomplete/commit/a83b00311909664d05b539de96a4f4acc18d9351)) 511 | - separate flag template for f.options && f.char ([3c6db62](https://github.com/oclif/plugin-autocomplete/commit/3c6db62e1df2651dfc6a58bf32d38c60ba5a9241)) 512 | - skip hidden commands ([264995d](https://github.com/oclif/plugin-autocomplete/commit/264995dc83d39276acaa00f926aec9481ced8e67)) 513 | - skip hidden flags ([a83ce0c](https://github.com/oclif/plugin-autocomplete/commit/a83ce0c5284217bf1acc85bd44dae039e0985eee)) 514 | - skip top-level commands without flags ([f0075d5](https://github.com/oclif/plugin-autocomplete/commit/f0075d538df189675d9b351de68945d8c8069be4)) 515 | - support `multiple` flag option ([ed51b14](https://github.com/oclif/plugin-autocomplete/commit/ed51b1481347ef3dc53498ee584aab35e3b75c75)) 516 | - wrap flag repeat spec in double quotes ([0d626fb](https://github.com/oclif/plugin-autocomplete/commit/0d626fb77d5288c011144fcf7f5d4fdac7848c36)) 517 | 518 | ### Features 519 | 520 | - file completion if no options are defined ([d64c5cc](https://github.com/oclif/plugin-autocomplete/commit/d64c5ccefad762c8cdf807174ba9ae75f6a8d44e)) 521 | - support cotopics ([8f7672c](https://github.com/oclif/plugin-autocomplete/commit/8f7672c501b109667d62ddc620cd7d9502b4184e)) 522 | - support global help flag ([be3dee4](https://github.com/oclif/plugin-autocomplete/commit/be3dee4c433e22e9c30f62a333cf87a66746f395)) 523 | - support space-separated cmds ([9a6c85a](https://github.com/oclif/plugin-autocomplete/commit/9a6c85a1d3a89238b3ce86770b81cfad69ef4718)) 524 | 525 | ## [1.4.6](https://github.com/oclif/plugin-autocomplete/compare/1.4.5...1.4.6) (2023-02-12) 526 | 527 | ### Bug Fixes 528 | 529 | - **deps:** bump @oclif/core from 2.0.8 to 2.1.2 ([1d3e0df](https://github.com/oclif/plugin-autocomplete/commit/1d3e0dfbd5f2a0519f135439d23c68cef93c5f75)) 530 | 531 | ## [1.4.5](https://github.com/oclif/plugin-autocomplete/compare/1.4.4...1.4.5) (2023-02-04) 532 | 533 | ### Bug Fixes 534 | 535 | - **deps:** bump http-cache-semantics from 4.1.0 to 4.1.1 ([7af4768](https://github.com/oclif/plugin-autocomplete/commit/7af4768a45ffec68b5fb7c2b97253ab90e4cf821)) 536 | 537 | ## [1.4.4](https://github.com/oclif/plugin-autocomplete/compare/1.4.3...1.4.4) (2023-01-31) 538 | 539 | ### Bug Fixes 540 | 541 | - support option flags with multiple=true ([#397](https://github.com/oclif/plugin-autocomplete/issues/397)) ([ee06dac](https://github.com/oclif/plugin-autocomplete/commit/ee06dacd25848c79e1586106541ec498f596417a)) 542 | 543 | ## [1.4.3](https://github.com/oclif/plugin-autocomplete/compare/1.4.2...1.4.3) (2023-01-29) 544 | 545 | ### Bug Fixes 546 | 547 | - **deps:** bump @oclif/core from 2.0.3 to 2.0.7 ([ad65d0f](https://github.com/oclif/plugin-autocomplete/commit/ad65d0f67021025aff6f8de2a25465007e735684)) 548 | 549 | ## [1.4.2](https://github.com/oclif/plugin-autocomplete/compare/1.4.1...1.4.2) (2023-01-23) 550 | 551 | ### Bug Fixes 552 | 553 | - bump core ([fcd5856](https://github.com/oclif/plugin-autocomplete/commit/fcd58564d902b611bdb70eefde2d0f26d6726af5)) 554 | 555 | ## [1.4.1](https://github.com/oclif/plugin-autocomplete/compare/1.4.0...1.4.1) (2023-01-19) 556 | 557 | ### Bug Fixes 558 | 559 | - **deps:** bump minimist from 1.2.5 to 1.2.7 ([8437741](https://github.com/oclif/plugin-autocomplete/commit/8437741d01b06a960266e2f323c364cfb70efa2a)) 560 | 561 | # [1.4.0](https://github.com/oclif/plugin-autocomplete/compare/1.3.10...1.4.0) (2023-01-18) 562 | 563 | ### Features 564 | 565 | - use oclif/core v2 ([bdb41ba](https://github.com/oclif/plugin-autocomplete/commit/bdb41ba671204ce9ca984de9095b7ad44269784e)) 566 | 567 | ## [1.3.10](https://github.com/oclif/plugin-autocomplete/compare/1.3.9...1.3.10) (2023-01-01) 568 | 569 | ### Bug Fixes 570 | 571 | - **deps:** bump json5 from 2.1.3 to 2.2.3 ([34d9233](https://github.com/oclif/plugin-autocomplete/commit/34d9233497a61a8d7d0e70a2349b5df45a349dea)) 572 | 573 | ## [1.3.9](https://github.com/oclif/plugin-autocomplete/compare/1.3.8...1.3.9) (2023-01-01) 574 | 575 | ### Bug Fixes 576 | 577 | - **deps:** bump @oclif/core from 1.22.0 to 1.23.1 ([16d7cb8](https://github.com/oclif/plugin-autocomplete/commit/16d7cb81a96dd13dc764e950497ff7e1be17a0e6)) 578 | 579 | ## [1.3.8](https://github.com/oclif/plugin-autocomplete/compare/1.3.7...1.3.8) (2022-12-18) 580 | 581 | ### Bug Fixes 582 | 583 | - **deps:** bump @oclif/core from 1.21.0 to 1.22.0 ([edc9cd9](https://github.com/oclif/plugin-autocomplete/commit/edc9cd92c023600c72e8ef167af13afa639812bc)) 584 | 585 | ## [1.3.7](https://github.com/oclif/plugin-autocomplete/compare/1.3.6...1.3.7) (2022-12-11) 586 | 587 | ### Bug Fixes 588 | 589 | - **deps:** bump @oclif/core from 1.20.4 to 1.21.0 ([c5ff9a5](https://github.com/oclif/plugin-autocomplete/commit/c5ff9a529b4bb315718cd41659965268084c2a60)) 590 | 591 | ## [1.3.6](https://github.com/oclif/plugin-autocomplete/compare/1.3.5...1.3.6) (2022-11-06) 592 | 593 | ### Bug Fixes 594 | 595 | - **deps:** bump @oclif/core from 1.20.0 to 1.20.4 ([a9a47d2](https://github.com/oclif/plugin-autocomplete/commit/a9a47d24ed07216fdf645e8f1c628972b37c5efa)) 596 | 597 | ## [1.3.5](https://github.com/oclif/plugin-autocomplete/compare/1.3.4...1.3.5) (2022-10-30) 598 | 599 | ### Bug Fixes 600 | 601 | - **deps:** bump @oclif/core from 1.19.1 to 1.20.0 ([80480bc](https://github.com/oclif/plugin-autocomplete/commit/80480bcb2ba584412135524dca866852a551a278)) 602 | 603 | ## [1.3.4](https://github.com/oclif/plugin-autocomplete/compare/1.3.3...1.3.4) (2022-10-23) 604 | 605 | ### Bug Fixes 606 | 607 | - **deps:** bump @oclif/core from 1.18.0 to 1.19.1 ([0d27549](https://github.com/oclif/plugin-autocomplete/commit/0d27549dd4b923be2c860d55477a90d45b87ca22)) 608 | 609 | ## [1.3.3](https://github.com/oclif/plugin-autocomplete/compare/1.3.2...1.3.3) (2022-10-16) 610 | 611 | ### Bug Fixes 612 | 613 | - **deps:** bump @oclif/core from 1.16.5 to 1.18.0 ([c96ccb9](https://github.com/oclif/plugin-autocomplete/commit/c96ccb9c7dd076c69bffa846d4b8623191ba629f)) 614 | 615 | ## [1.3.2](https://github.com/oclif/plugin-autocomplete/compare/1.3.1...1.3.2) (2022-10-09) 616 | 617 | ### Bug Fixes 618 | 619 | - **deps:** bump @oclif/core from 1.16.4 to 1.16.5 ([696a6ed](https://github.com/oclif/plugin-autocomplete/commit/696a6edd72fa4ed93392d70a9fa18945901b528d)) 620 | 621 | ## [1.3.1](https://github.com/oclif/plugin-autocomplete/compare/v1.3.0...1.3.1) (2022-09-27) 622 | 623 | ### Bug Fixes 624 | 625 | - **deps:** bump @oclif/core from 1.7.0 to 1.16.4 ([6aaa72b](https://github.com/oclif/plugin-autocomplete/commit/6aaa72beb449050eef61a969d17c543081bf57e8)) 626 | 627 | # [1.3.0](https://github.com/oclif/plugin-autocomplete/compare/v1.2.0...v1.3.0) (2022-05-03) 628 | 629 | ### Features 630 | 631 | - aliases support ([a0db49e](https://github.com/oclif/plugin-autocomplete/commit/a0db49e48054f6979f3bf52fe6fbdf590e865d68)) 632 | 633 | # [1.2.0](https://github.com/oclif/plugin-autocomplete/compare/v1.1.1...v1.2.0) (2022-01-28) 634 | 635 | ### Features 636 | 637 | - remove cli-ux ([2107bc0](https://github.com/oclif/plugin-autocomplete/commit/2107bc0f371fc185e1678e3ee33888b4968ad262)) 638 | 639 | ## [1.1.1](https://github.com/oclif/plugin-autocomplete/compare/v1.1.0...v1.1.1) (2022-01-06) 640 | 641 | ### Bug Fixes 642 | 643 | - bump cli-ux ([fd07ca5](https://github.com/oclif/plugin-autocomplete/commit/fd07ca5c76c0735068e8cd0558dad2add2fdfd19)) 644 | 645 | # [1.1.0](https://github.com/oclif/plugin-autocomplete/compare/v1.0.0...v1.1.0) (2022-01-04) 646 | 647 | ### Bug Fixes 648 | 649 | - test ([a9a668c](https://github.com/oclif/plugin-autocomplete/commit/a9a668c1626dc5b8f711014bc4c95dabebd949d1)) 650 | - whoops, did not mean to include that file ([705d7db](https://github.com/oclif/plugin-autocomplete/commit/705d7db00626304ac30ad3b1824c88fc8dcd85b8)) 651 | 652 | ### Features 653 | 654 | - remove moment ([79dc37b](https://github.com/oclif/plugin-autocomplete/commit/79dc37b37600f294c02e85469c1664c83849ff30)) 655 | 656 | # [1.0.0](https://github.com/oclif/plugin-autocomplete/compare/v0.3.0...v1.0.0) (2021-12-14) 657 | 658 | ### Bug Fixes 659 | 660 | - add unit test ([c544ba8](https://github.com/oclif/plugin-autocomplete/commit/c544ba84a0229936f8842c95aa9c11904514f90e)) 661 | - escape chars ([14daa6a](https://github.com/oclif/plugin-autocomplete/commit/14daa6af896fbea5807cd002445e6c32649e28fd)) 662 | - include complete command, whoops. ([9b53a63](https://github.com/oclif/plugin-autocomplete/commit/9b53a63eb9b51d0b8e6750071c6500d42792a35d)) 663 | - properly encapsulate expanded arrays in "" ([979be23](https://github.com/oclif/plugin-autocomplete/commit/979be23f22983e848f957fcdcfa14bce8d067ddd)) 664 | - remove console.log ([2c6e060](https://github.com/oclif/plugin-autocomplete/commit/2c6e0603161fa68f19686e1a0cbd6b50aeb0aaf8)) 665 | - unit tests & lint ([f0bfd35](https://github.com/oclif/plugin-autocomplete/commit/f0bfd352fa4f626a2596d5c4e9e0d9b715ea6249)) 666 | 667 | ### Features 668 | 669 | - add support for commands separated by spaces (WIP) ([6b5248f](https://github.com/oclif/plugin-autocomplete/commit/6b5248fe28a80e432c8608a91232417b2a0dc073)) 670 | - use oclif/core ([#251](https://github.com/oclif/plugin-autocomplete/issues/251)) ([f781819](https://github.com/oclif/plugin-autocomplete/commit/f781819989fa66ace0461a91a49f4cc37ff83c64)) 671 | 672 | # [0.3.0](https://github.com/oclif/plugin-autocomplete/compare/v0.2.1...v0.3.0) (2020-12-17) 673 | 674 | ### Bug Fixes 675 | 676 | - completion for subcommands ([#126](https://github.com/oclif/plugin-autocomplete/issues/126)) ([30b2857](https://github.com/oclif/plugin-autocomplete/commit/30b2857ed5f0ee9557d150e9ac75038f012c8964)), closes [oclif#9](https://github.com/oclif/issues/9) 677 | - **zsh:** update zsh autocomplete to work with default settings ([#92](https://github.com/oclif/plugin-autocomplete/issues/92)) ([b9e8e7b](https://github.com/oclif/plugin-autocomplete/commit/b9e8e7ba2946c5b16d8af32b70cfffa062880572)), closes [#91](https://github.com/oclif/plugin-autocomplete/issues/91) 678 | 679 | ### Features 680 | 681 | - install on windows bash ([#34](https://github.com/oclif/plugin-autocomplete/issues/34)) ([4ca20e5](https://github.com/oclif/plugin-autocomplete/commit/4ca20e5f840c715fe658f0a12840e34783793995)) 682 | 683 | ## [0.2.1](https://github.com/oclif/plugin-autocomplete/compare/v0.2.0...v0.2.1) (2020-12-11) 684 | 685 | ### Reverts 686 | 687 | - Revert "chore(deps-dev): bump @types/fs-extra from 9.0.1 to 9.0.5 (#163)" (#164) ([0d88c1a](https://github.com/oclif/plugin-autocomplete/commit/0d88c1a68ffb824d8094163ab858a885990b9c76)), closes [#163](https://github.com/oclif/plugin-autocomplete/issues/163) [#164](https://github.com/oclif/plugin-autocomplete/issues/164) 688 | 689 | # [0.2.0](https://github.com/oclif/plugin-autocomplete/compare/v0.1.5...v0.2.0) (2020-05-05) 690 | 691 | ### Features 692 | 693 | - update dependencies ([#40](https://github.com/oclif/plugin-autocomplete/issues/40)) ([4b18c97](https://github.com/oclif/plugin-autocomplete/commit/4b18c97c5ce3f88b51535b2c74cbc0906391d34a)) 694 | 695 | ## [0.1.5](https://github.com/oclif/plugin-autocomplete/compare/v0.1.4...v0.1.5) (2019-12-03) 696 | 697 | ### Bug Fixes 698 | 699 | - fallback to file completion if no matches found ([#33](https://github.com/oclif/plugin-autocomplete/issues/33)) ([41b7827](https://github.com/oclif/plugin-autocomplete/commit/41b78271488f2a874ba30aa7b07d53fafbb733a8)) 700 | 701 | ## [0.1.4](https://github.com/oclif/plugin-autocomplete/compare/v0.1.3...v0.1.4) (2019-09-05) 702 | 703 | ### Bug Fixes 704 | 705 | - only use the first line of command/flag descriptions for zsh ([#29](https://github.com/oclif/plugin-autocomplete/issues/29)) ([f3d0e94](https://github.com/oclif/plugin-autocomplete/commit/f3d0e944edcb601a17079d3e05f465103fa540fe)) 706 | - sanitize double-quotes in description of commands/flags ([#28](https://github.com/oclif/plugin-autocomplete/issues/28)) ([d8a1fd5](https://github.com/oclif/plugin-autocomplete/commit/d8a1fd5a6ebfa982c5f483bc26b7ea3d5d8bb66f)) 707 | 708 | ## [0.1.3](https://github.com/oclif/plugin-autocomplete/compare/v0.1.2...v0.1.3) (2019-08-01) 709 | 710 | ### Bug Fixes 711 | 712 | - sanitize square brackets in description of commands/flags ([#16](https://github.com/oclif/plugin-autocomplete/issues/16)) ([3bf4821](https://github.com/oclif/plugin-autocomplete/commit/3bf4821ac78bcda65750027657193cab20f445d4)) 713 | 714 | ## [0.1.2](https://github.com/oclif/plugin-autocomplete/compare/v0.1.1...v0.1.2) (2019-07-24) 715 | 716 | ### Bug Fixes 717 | 718 | - sanitize backtick in description of commands/flags ([#12](https://github.com/oclif/plugin-autocomplete/issues/12)) ([65d968f](https://github.com/oclif/plugin-autocomplete/commit/65d968f561db724e81ee4c2f4ff569ed77d69d02)) 719 | 720 | ## [0.1.1](https://github.com/oclif/plugin-autocomplete/compare/3446a6b0bd700c2c94c6f94f3c95115041ef0b4d...v0.1.1) (2019-06-05) 721 | 722 | ### Bug Fixes 723 | 724 | - only use the first line of the command description for zsh ([#10](https://github.com/oclif/plugin-autocomplete/issues/10)) ([3446a6b](https://github.com/oclif/plugin-autocomplete/commit/3446a6b0bd700c2c94c6f94f3c95115041ef0b4d)) 725 | -------------------------------------------------------------------------------- /CONRTIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development 4 | 5 | **1. Clone this repository...** 6 | 7 | ```bash 8 | $ git clone git@github.com:oclif/plugin-autocomplete.git 9 | ``` 10 | 11 | **2. Navigate into project & install development-specific dependencies...** 12 | 13 | ```bash 14 | $ cd ./plugin-autocomplete && yarn 15 | ``` 16 | 17 | **3. Write some code &/or add some tests...** 18 | 19 | ```bash 20 | ... 21 | ``` 22 | 23 | **4. Test changes locally** 24 | 25 | To test using local dev script: 26 | 27 | ``` 28 | ./bin/dev.js 29 | ``` 30 | 31 | To test inside an existing oclif CLI (must have [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins) installed): 32 | 33 | ``` 34 | plugins link --no-install 35 | 36 | ``` 37 | 38 | See `plugins link` [documentation](https://github.com/oclif/plugin-plugins?tab=readme-ov-file#mycli-pluginslink-plugin) 39 | 40 | **5. Run tests & ensure they pass...** 41 | 42 | ``` 43 | $ yarn test 44 | ``` 45 | 46 | **6. Open a [Pull Request](https://github.com/oclif/plugin-autocomplete/pulls) for your work & become the newest contributor to `@oclif/plugin-autocomplete`! 🎉** 47 | 48 | ## Pull Request Conventions 49 | 50 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). When opening a pull request, please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 51 | 52 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 53 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 54 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 55 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be _no_ change to the version of the package when it is next published (as the commit does not affect the published version). 56 | 57 | ## What _not_ to contribute? 58 | 59 | ### Dependencies 60 | 61 | It should be noted that our team does not accept third-party dependency updates/PRs. We use dependabot to ensure dependencies are staying up-to-date & will ship security patches for CVEs as they occur. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 62 | 63 | ### Tools/Automation 64 | 65 | Our core team is responsible for the maintenance of the tooling/automation in this project & we ask collaborators to kindly not make changes to these when contributing (ex. `.github/*`, `.eslintrc.json`, package.json `scripts`, etc.) 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Salesforce.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @oclif/plugin-autocomplete 2 | 3 | autocomplete plugin for oclif (bash, zsh and powershell) 4 | 5 | [![Version](https://img.shields.io/npm/v/@oclif/plugin-autocomplete.svg)](https://npmjs.org/package/@oclif/plugin-autocomplete) 6 | [![Downloads/week](https://img.shields.io/npm/dw/@oclif/plugin-autocomplete.svg)](https://npmjs.org/package/@oclif/plugin-autocomplete) 7 | [![License](https://img.shields.io/npm/l/@oclif/plugin-autocomplete.svg)](https://github.com/oclif/plugin-autocomplete/blob/main/package.json) 8 | 9 | 10 | 11 | - [@oclif/plugin-autocomplete](#oclifplugin-autocomplete) 12 | - [Usage](#usage) 13 | - [Commands](#commands) 14 | - [Contributing](#contributing) 15 | 16 | 17 | # Usage 18 | 19 | Run ` autocomplete` to generate the autocomplete files for your current shell. 20 | 21 | ## Topic separator 22 | 23 | Since oclif v2 it's possible to use spaces as a topic separator in addition to colons. 24 | 25 | For bash and zsh each topic separator has different autocomplete implementations, if the CLI supports using a space as the separator, plugin-autocomplete will generate completion for that topic. 26 | 27 | If you still want to use the colon-separated autocomplete you can set `OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR` to `colon` and re-generate the autocomplete files. 28 | 29 | Docs: https://oclif.io/docs/topic_separator 30 | 31 | # Commands 32 | 33 | 34 | 35 | - [`oclif-example autocomplete [SHELL]`](#oclif-example-autocomplete-shell) 36 | 37 | ## `oclif-example autocomplete [SHELL]` 38 | 39 | Display autocomplete installation instructions. 40 | 41 | ``` 42 | USAGE 43 | $ oclif-example autocomplete [SHELL] [-r] 44 | 45 | ARGUMENTS 46 | SHELL (zsh|bash|powershell) Shell type 47 | 48 | FLAGS 49 | -r, --refresh-cache Refresh cache (ignores displaying instructions) 50 | 51 | DESCRIPTION 52 | Display autocomplete installation instructions. 53 | 54 | EXAMPLES 55 | $ oclif-example autocomplete 56 | 57 | $ oclif-example autocomplete bash 58 | 59 | $ oclif-example autocomplete zsh 60 | 61 | $ oclif-example autocomplete powershell 62 | 63 | $ oclif-example autocomplete --refresh-cache 64 | ``` 65 | 66 | _See code: [src/commands/autocomplete/index.ts](https://github.com/oclif/plugin-autocomplete/blob/3.2.29/src/commands/autocomplete/index.ts)_ 67 | 68 | 69 | 70 | # Contributing 71 | 72 | See [contributing guide](./CONRTIBUTING.md) 73 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | 3 | async function main() { 4 | const {execute} = await import('@oclif/core') 5 | await execute({development: true, dir: import.meta.url}) 6 | } 7 | 8 | await main() 9 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | async function main() { 4 | const {execute} = await import('@oclif/core') 5 | await execute({dir: import.meta.url}) 6 | } 7 | 8 | await main() 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import {includeIgnoreFile} from '@eslint/compat' 2 | import oclif from 'eslint-config-oclif' 3 | import prettier from 'eslint-config-prettier' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore') 8 | 9 | export default [ 10 | includeIgnoreFile(gitignorePath), 11 | ...oclif, 12 | prettier, 13 | { 14 | rules: { 15 | '@typescript-eslint/no-explicit-any': 'off', 16 | 'max-depth': ['warn', 5], 17 | 'unicorn/consistent-destructuring': 'off', 18 | 'unicorn/prefer-string-raw': 'off', 19 | }, 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oclif/plugin-autocomplete", 3 | "description": "autocomplete plugin for oclif", 4 | "version": "3.2.29", 5 | "author": "Salesforce", 6 | "bugs": "https://github.com/oclif/plugin-autocomplete/issues", 7 | "dependencies": { 8 | "@oclif/core": "^4", 9 | "ansis": "^3.16.0", 10 | "debug": "^4.4.1", 11 | "ejs": "^3.1.10" 12 | }, 13 | "devDependencies": { 14 | "@commitlint/config-conventional": "^19", 15 | "@eslint/compat": "^1.2.9", 16 | "@oclif/plugin-help": "^6", 17 | "@oclif/prettier-config": "^0.2.1", 18 | "@oclif/test": "^4", 19 | "@types/chai": "^4", 20 | "@types/debug": "^4.1.12", 21 | "@types/ejs": "^3.1.5", 22 | "@types/mocha": "^10.0.9", 23 | "@types/nock": "^11.1.0", 24 | "@types/node": "^18", 25 | "chai": "^4", 26 | "commitlint": "^19", 27 | "eslint": "^9.28.0", 28 | "eslint-config-oclif": "^6.0.66", 29 | "eslint-config-prettier": "^10.1.5", 30 | "husky": "^9.1.7", 31 | "lint-staged": "^15.5.2", 32 | "mocha": "^10.8.2", 33 | "nock": "^13.5.6", 34 | "nyc": "^15.1.0", 35 | "oclif": "^4.17.46", 36 | "prettier": "^3.5.3", 37 | "shx": "^0.4.0", 38 | "ts-node": "^10.9.2", 39 | "typescript": "^5.7.3" 40 | }, 41 | "engines": { 42 | "node": ">=18.0.0" 43 | }, 44 | "exports": "./lib/index.js", 45 | "files": [ 46 | "oclif.manifest.json", 47 | "/lib" 48 | ], 49 | "homepage": "https://github.com/oclif/plugin-autocomplete", 50 | "keywords": [ 51 | "oclif-plugin" 52 | ], 53 | "license": "MIT", 54 | "oclif": { 55 | "commands": "./lib/commands", 56 | "bin": "oclif-example", 57 | "devPlugins": [ 58 | "@oclif/plugin-help" 59 | ], 60 | "flexibleTaxonomy": true, 61 | "hooks": { 62 | "plugins:postinstall": "./lib/hooks/refresh-cache.js", 63 | "plugins:postuninstall": "./lib/hooks/refresh-cache.js" 64 | } 65 | }, 66 | "repository": "oclif/plugin-autocomplete", 67 | "scripts": { 68 | "build": "shx rm -rf lib && tsc", 69 | "clean": "shx rm -f oclif.manifest.json", 70 | "compile": "tsc", 71 | "lint": "eslint", 72 | "postpack": "yarn run clean", 73 | "posttest": "yarn lint", 74 | "prepack": "yarn build && oclif manifest && oclif readme", 75 | "prepare": "husky && yarn build", 76 | "pretest": "yarn build && tsc -p test --noEmit", 77 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 78 | "version": "oclif readme && git add README.md" 79 | }, 80 | "type": "module" 81 | } 82 | -------------------------------------------------------------------------------- /src/autocomplete/bash-spaces.ts: -------------------------------------------------------------------------------- 1 | const script = `#!/usr/bin/env bash 2 | 3 | # This function joins an array using a character passed in 4 | # e.g. ARRAY=(one two three) -> join_by ":" \${ARRAY[@]} -> "one:two:three" 5 | function join_by { local IFS="$1"; shift; echo "$*"; } 6 | 7 | __autocomplete() 8 | { 9 | 10 | local cur="\${COMP_WORDS[COMP_CWORD]}" opts normalizedCommand colonPrefix IFS=$' \\t\\n' 11 | COMPREPLY=() 12 | 13 | local commands=" 14 | 15 | " 16 | 17 | function __trim_colon_commands() 18 | { 19 | # Turn $commands into an array 20 | commands=("\${commands[@]}") 21 | 22 | if [[ -z "$colonPrefix" ]]; then 23 | colonPrefix="$normalizedCommand:" 24 | fi 25 | 26 | # Remove colon-word prefix from $commands 27 | commands=( "\${commands[@]/$colonPrefix}" ) 28 | 29 | for i in "\${!commands[@]}"; do 30 | if [[ "\${commands[$i]}" == "$normalizedCommand" ]]; then 31 | # If the currently typed in command is a topic command we need to remove it to avoid suggesting it again 32 | unset "\${commands[$i]}" 33 | else 34 | # Trim subcommands from each command 35 | commands[$i]="\${commands[$i]%%:*}" 36 | fi 37 | done 38 | } 39 | 40 | if [[ "$cur" != "-"* ]]; then 41 | # Command 42 | __COMP_WORDS=( "\${COMP_WORDS[@]:1}" ) 43 | 44 | # The command typed by the user but separated by colons (e.g. "mycli command subcom" -> "command:subcom") 45 | normalizedCommand="$( printf "%s" "$(join_by ":" "\${__COMP_WORDS[@]}")" )" 46 | 47 | # The command hirarchy, with colons, leading up to the last subcommand entered (e.g. "mycli com subcommand subsubcom" -> "com:subcommand:") 48 | colonPrefix="\${normalizedCommand%"\${normalizedCommand##*:}"}" 49 | 50 | if [[ -z "$normalizedCommand" ]]; then 51 | # If there is no normalizedCommand yet the user hasn't typed in a full command 52 | # So we should trim all subcommands & flags from $commands so we can suggest all top level commands 53 | opts=$(printf "%s " "\${commands[@]}" | grep -Eo '^[a-zA-Z0-9_-]+') 54 | else 55 | # Filter $commands to just the ones that match the $normalizedCommand and turn into an array 56 | commands=( $(compgen -W "$commands" -- "\${normalizedCommand}") ) 57 | # Trim higher level and subcommands from the subcommands to suggest 58 | __trim_colon_commands "$colonPrefix" 59 | 60 | opts=$(printf "%s " "\${commands[@]}") # | grep -Eo '^[a-zA-Z0-9_-]+' 61 | fi 62 | else 63 | # Flag 64 | 65 | # The full CLI command separated by colons (e.g. "mycli command subcommand --fl" -> "command:subcommand") 66 | # This needs to be defined with $COMP_CWORD-1 as opposed to above because the current "word" on the command line is a flag and the command is everything before the flag 67 | normalizedCommand="$( printf "%s" "$(join_by ":" "\${COMP_WORDS[@]:1:($COMP_CWORD - 1)}")" )" 68 | 69 | # The line below finds the command in $commands using grep 70 | # Then, using sed, it removes everything from the found command before the --flags (e.g. "command:subcommand:subsubcom --flag1 --flag2" -> "--flag1 --flag2") 71 | opts=$(printf "%s " "\${commands[@]}" | grep "\${normalizedCommand}" | sed -n "s/^\${normalizedCommand} //p") 72 | fi 73 | 74 | COMPREPLY=($(compgen -W "$opts" -- "\${cur}")) 75 | } 76 | 77 | complete -F __autocomplete 78 | ` 79 | 80 | export default script 81 | -------------------------------------------------------------------------------- /src/autocomplete/bash.ts: -------------------------------------------------------------------------------- 1 | const script = `#!/usr/bin/env bash 2 | 3 | __autocomplete() 4 | { 5 | 6 | local cur="\${COMP_WORDS[COMP_CWORD]}" opts IFS=$' \\t\\n' 7 | COMPREPLY=() 8 | 9 | local commands=" 10 | 11 | " 12 | 13 | if [[ "$cur" != "-"* ]]; then 14 | opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+') 15 | else 16 | local __COMP_WORDS 17 | if [[ \${COMP_WORDS[2]} == ":" ]]; then 18 | #subcommand 19 | __COMP_WORDS=$(printf "%s" "\${COMP_WORDS[@]:1:3}") 20 | else 21 | #simple command 22 | __COMP_WORDS="\${COMP_WORDS[@]:1:1}" 23 | fi 24 | opts=$(printf "$commands" | grep "\${__COMP_WORDS}" | sed -n "s/^\${__COMP_WORDS} //p") 25 | fi 26 | _get_comp_words_by_ref -n : cur 27 | COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) ) 28 | __ltrim_colon_completions "$cur" 29 | return 0 30 | 31 | } 32 | 33 | complete -o default -F __autocomplete 34 | ` 35 | 36 | export default script 37 | -------------------------------------------------------------------------------- /src/autocomplete/powershell.ts: -------------------------------------------------------------------------------- 1 | import {Command, Config, Interfaces} from '@oclif/core' 2 | import * as ejs from 'ejs' 3 | import {EOL} from 'node:os' 4 | import {format} from 'node:util' 5 | 6 | type CommandCompletion = { 7 | flags: CommandFlags 8 | id: string 9 | summary: string 10 | } 11 | 12 | type CommandFlags = { 13 | [name: string]: Command.Flag.Cached 14 | } 15 | 16 | type Topic = { 17 | description: string 18 | name: string 19 | } 20 | 21 | export default class PowerShellComp { 22 | protected config: Config 23 | private _coTopics?: string[] 24 | private commands: CommandCompletion[] 25 | private topics: Topic[] 26 | 27 | constructor(config: Config) { 28 | this.config = config 29 | this.topics = this.getTopics() 30 | this.commands = this.getCommands() 31 | } 32 | 33 | private get coTopics(): string[] { 34 | if (this._coTopics) return this._coTopics 35 | 36 | const coTopics: string[] = [] 37 | 38 | for (const topic of this.topics) { 39 | for (const cmd of this.commands) { 40 | if (topic.name === cmd.id) { 41 | coTopics.push(topic.name) 42 | } 43 | } 44 | } 45 | 46 | this._coTopics = coTopics 47 | 48 | return this._coTopics 49 | } 50 | 51 | public generate(): string { 52 | const genNode = (partialId: string): Record => { 53 | const node: Record = {} 54 | 55 | const nextArgs: string[] = [] 56 | 57 | const depth = partialId.split(':').length 58 | 59 | for (const t of this.topics) { 60 | const topicNameSplit = t.name.split(':') 61 | 62 | if (t.name.startsWith(partialId + ':') && topicNameSplit.length === depth + 1) { 63 | nextArgs.push(topicNameSplit[depth]) 64 | 65 | node[topicNameSplit[depth]] = this.coTopics.includes(t.name) 66 | ? { 67 | ...genNode(`${partialId}:${topicNameSplit[depth]}`), 68 | } 69 | : { 70 | _summary: t.description, 71 | ...genNode(`${partialId}:${topicNameSplit[depth]}`), 72 | } 73 | } 74 | } 75 | 76 | for (const c of this.commands) { 77 | const cmdIdSplit = c.id.split(':') 78 | 79 | if (partialId === c.id && this.coTopics.includes(c.id)) { 80 | node._command = c.id 81 | } 82 | 83 | if ( 84 | c.id.startsWith(partialId + ':') && 85 | cmdIdSplit.length === depth + 1 && 86 | !nextArgs.includes(cmdIdSplit[depth]) 87 | ) { 88 | node[cmdIdSplit[depth]] = { 89 | _command: c.id, 90 | } 91 | } 92 | } 93 | 94 | return node 95 | } 96 | 97 | const commandTree: Record = {} 98 | 99 | const topLevelArgs: string[] = [] 100 | 101 | // Collect top-level topics and generate a cmd tree node for each one of them. 102 | for (const t of this.topics) { 103 | if (!t.name.includes(':')) { 104 | commandTree[t.name] = this.coTopics.includes(t.name) 105 | ? { 106 | ...genNode(t.name), 107 | } 108 | : { 109 | _summary: t.description, 110 | ...genNode(t.name), 111 | } 112 | 113 | topLevelArgs.push(t.name) 114 | } 115 | } 116 | 117 | // Collect top-level commands and add a cmd tree node with the command ID. 118 | for (const c of this.commands) { 119 | if (!c.id.includes(':') && !this.coTopics.includes(c.id)) { 120 | commandTree[c.id] = { 121 | _command: c.id, 122 | } 123 | 124 | topLevelArgs.push(c.id) 125 | } 126 | } 127 | 128 | const hashtables: string[] = [] 129 | 130 | for (const topLevelArg of topLevelArgs) { 131 | // Generate all the hashtables for each child node of a top-level arg. 132 | hashtables.push(this.genHashtable(topLevelArg, commandTree)) 133 | } 134 | 135 | const commandsHashtable = ` 136 | @{ 137 | ${hashtables.join('\n')} 138 | }` 139 | 140 | const compRegister = ` 141 | using namespace System.Management.Automation 142 | using namespace System.Management.Automation.Language 143 | 144 | $scriptblock = { 145 | param($WordToComplete, $CommandAst, $CursorPosition) 146 | 147 | $Commands =${commandsHashtable} 148 | 149 | # Get the current mode 150 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 151 | 152 | # Everything in the current line except the CLI executable name. 153 | $CurrentLine = $commandAst.CommandElements[1..$commandAst.CommandElements.Count] -split " " 154 | 155 | # Remove $WordToComplete from the current line. 156 | if ($WordToComplete -ne "") { 157 | if ($CurrentLine.Count -eq 1) { 158 | $CurrentLine = @() 159 | } else { 160 | $CurrentLine = $CurrentLine[0..$CurrentLine.Count] 161 | } 162 | } 163 | 164 | # Save flags in current line without the \`--\` prefix. 165 | $Flags = $CurrentLine | Where-Object { 166 | $_ -Match "^-{1,2}(\\w+)" 167 | } | ForEach-Object { 168 | $_.trim("-") 169 | } 170 | # Set $flags to an empty hashtable if there are no flags in the current line. 171 | if ($Flags -eq $null) { 172 | $Flags = @{} 173 | } 174 | 175 | # No command in the current line, suggest top-level args. 176 | if ($CurrentLine.Count -eq 0) { 177 | $Commands.GetEnumerator() | Where-Object { 178 | $_.Key.StartsWith("$WordToComplete") 179 | } | Sort-Object -Property key | ForEach-Object { 180 | New-Object -Type CompletionResult -ArgumentList \` 181 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 182 | $_.Key, 183 | "ParameterValue", 184 | "$($_.Value._summary ?? $_.Value._command.summary ?? " ")" 185 | } 186 | } else { 187 | # Start completing command/topic/coTopic 188 | 189 | $NextArg = $null 190 | $PrevNode = $null 191 | 192 | # Iterate over the current line to find the command/topic/coTopic hashtable 193 | $CurrentLine | ForEach-Object { 194 | if ($NextArg -eq $null) { 195 | $NextArg = $Commands[$_] 196 | } elseif ($PrevNode[$_] -ne $null) { 197 | $NextArg = $PrevNode[$_] 198 | } elseif ($_.StartsWith('-')) { 199 | return 200 | } else { 201 | $NextArg = $PrevNode 202 | } 203 | 204 | $PrevNode = $NextArg 205 | } 206 | 207 | # Start completing command. 208 | if ($NextArg._command -ne $null) { 209 | # Complete flags 210 | # \`cli config list -\` 211 | if ($WordToComplete -like '-*') { 212 | $NextArg._command.flags.GetEnumerator() | Sort-Object -Property key 213 | | Where-Object { 214 | # Filter out already used flags (unless \`flag.multiple = true\`). 215 | $_.Key.StartsWith("$($WordToComplete.Trim("-"))") -and ($_.Value.multiple -eq $true -or !$flags.Contains($_.Key)) 216 | } 217 | | ForEach-Object { 218 | New-Object -Type CompletionResult -ArgumentList \` 219 | $($Mode -eq "MenuComplete" ? "--$($_.Key) " : "--$($_.Key)"), 220 | $_.Key, 221 | "ParameterValue", 222 | "$($NextArg._command.flags[$_.Key].summary ?? " ")" 223 | } 224 | } else { 225 | # This could be a coTopic. We remove the "_command" hashtable 226 | # from $NextArg and check if there's a command under the current partial ID. 227 | $NextArg.remove("_command") 228 | 229 | if ($NextArg.keys -gt 0) { 230 | $NextArg.GetEnumerator() | Where-Object { 231 | $_.Key.StartsWith("$WordToComplete") 232 | } | Sort-Object -Property key | ForEach-Object { 233 | New-Object -Type CompletionResult -ArgumentList \` 234 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 235 | $_.Key, 236 | "ParameterValue", 237 | "$($NextArg[$_.Key]._summary ?? " ")" 238 | } 239 | } 240 | } 241 | } else { 242 | # Start completing topic. 243 | 244 | # Topic summary is stored as "_summary" in the hashtable. 245 | # At this stage it is no longer needed so we remove it 246 | # so that $NextArg contains only commands/topics hashtables 247 | 248 | $NextArg.remove("_summary") 249 | 250 | $NextArg.GetEnumerator() | Where-Object { 251 | $_.Key.StartsWith("$WordToComplete") 252 | } | Sort-Object -Property key | ForEach-Object { 253 | New-Object -Type CompletionResult -ArgumentList \` 254 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 255 | $_.Key, 256 | "ParameterValue", 257 | "$($NextArg[$_.Key]._summary ?? $NextArg[$_.Key]._command.summary ?? " ")" 258 | } 259 | } 260 | } 261 | } 262 | Register-ArgumentCompleter -Native -CommandName ${ 263 | this.config.binAliases 264 | ? `@(${[...this.config.binAliases, this.config.bin].map((alias) => `"${alias}"`).join(',')})` 265 | : this.config.bin 266 | } -ScriptBlock $scriptblock 267 | ` 268 | 269 | return compRegister 270 | } 271 | 272 | private genCmdHashtable(cmd: CommandCompletion): string { 273 | const flaghHashtables: string[] = [] 274 | 275 | const flagNames = Object.keys(cmd.flags) 276 | 277 | // Add comp for the global `--help` flag. 278 | if (!flagNames.includes('help')) { 279 | flaghHashtables.push(' "help" = @{ "summary" = "Show help for command" }') 280 | } 281 | 282 | if (flagNames.length > 0) { 283 | for (const flagName of flagNames) { 284 | const f = cmd.flags[flagName] 285 | // skip hidden flags 286 | if (f.hidden) continue 287 | 288 | const flagSummary = this.sanitizeSummary(f.summary ?? f.description) 289 | 290 | if (f.type === 'option' && f.multiple) { 291 | flaghHashtables.push( 292 | ` "${f.name}" = @{ 293 | "summary" = "${flagSummary}" 294 | "multiple" = $true 295 | }`, 296 | ) 297 | } else { 298 | flaghHashtables.push(` "${f.name}" = @{ "summary" = "${flagSummary}" }`) 299 | } 300 | } 301 | } 302 | 303 | const cmdHashtable = `@{ 304 | "summary" = "${cmd.summary}" 305 | "flags" = @{ 306 | ${flaghHashtables.join('\n')} 307 | } 308 | }` 309 | return cmdHashtable 310 | } 311 | 312 | private genHashtable(key: string, node: Record, leafTpl?: string): string { 313 | if (!leafTpl) { 314 | leafTpl = `"${key}" = @{ 315 | %s 316 | } 317 | ` 318 | } 319 | 320 | const nodeKeys = Object.keys(node[key]) 321 | 322 | // this is a topic 323 | if (nodeKeys.includes('_summary')) { 324 | let childTpl = `"_summary" = "${node[key]._summary}"\n%s` 325 | 326 | const newKeys = nodeKeys.filter((k) => k !== '_summary') 327 | if (newKeys.length > 0) { 328 | const childNodes: string[] = [] 329 | 330 | for (const newKey of newKeys) { 331 | childNodes.push(this.genHashtable(newKey, node[key])) 332 | } 333 | 334 | childTpl = format(childTpl, childNodes.join('\n')) 335 | 336 | return format(leafTpl, childTpl) 337 | } 338 | 339 | // last node 340 | return format(leafTpl, childTpl) 341 | } 342 | 343 | const childNodes: string[] = [] 344 | for (const k of nodeKeys) { 345 | if (k === '_command') { 346 | const cmd = this.commands.find((c) => c.id === node[key][k]) 347 | if (!cmd) throw new Error('no command') 348 | 349 | childNodes.push(format('"_command" = %s', this.genCmdHashtable(cmd))) 350 | } else if (node[key][k]._command) { 351 | const cmd = this.commands.find((c) => c.id === node[key][k]._command) 352 | if (!cmd) throw new Error('no command') 353 | 354 | childNodes.push(format(`"${k}" = @{\n"_command" = %s\n}`, this.genCmdHashtable(cmd))) 355 | } else { 356 | const childTpl = `"summary" = "${node[key][k]._summary}"\n"${k}" = @{ \n %s\n }` 357 | childNodes.push(this.genHashtable(k, node[key], childTpl)) 358 | } 359 | } 360 | 361 | if (childNodes.length > 0) { 362 | return format(leafTpl, childNodes.join('\n')) 363 | } 364 | 365 | return leafTpl 366 | } 367 | 368 | private getCommands(): CommandCompletion[] { 369 | const cmds: CommandCompletion[] = [] 370 | 371 | for (const p of this.config.getPluginsList()) { 372 | for (const c of p.commands) { 373 | if (c.hidden) continue 374 | const summary = this.sanitizeSummary(c.summary ?? c.description) 375 | const {flags} = c 376 | cmds.push({ 377 | flags, 378 | id: c.id, 379 | summary, 380 | }) 381 | 382 | for (const a of c.aliases) { 383 | cmds.push({ 384 | flags, 385 | id: a, 386 | summary, 387 | }) 388 | 389 | const split = a.split(':') 390 | 391 | let topic = split[0] 392 | 393 | // Completion funcs are generated from topics: 394 | // `force` -> `force:org` -> `force:org:open|list` 395 | // 396 | // but aliases aren't guaranteed to follow the plugin command tree 397 | // so we need to add any missing topic between the starting point and the alias. 398 | for (let i = 0; i < split.length - 1; i++) { 399 | if (!this.topics.some((t) => t.name === topic)) { 400 | this.topics.push({ 401 | description: `${topic.replaceAll(':', ' ')} commands`, 402 | name: topic, 403 | }) 404 | } 405 | 406 | topic += `:${split[i + 1]}` 407 | } 408 | } 409 | } 410 | } 411 | 412 | return cmds 413 | } 414 | 415 | private getTopics(): Topic[] { 416 | const topics = this.config.topics 417 | .filter((topic: Interfaces.Topic) => { 418 | // it is assumed a topic has a child if it has children 419 | const hasChild = this.config.topics.some((subTopic) => subTopic.name.includes(`${topic.name}:`)) 420 | return hasChild 421 | }) 422 | .sort((a, b) => { 423 | if (a.name < b.name) { 424 | return -1 425 | } 426 | 427 | if (a.name > b.name) { 428 | return 1 429 | } 430 | 431 | return 0 432 | }) 433 | .map((t) => { 434 | const description = t.description 435 | ? this.sanitizeSummary(t.description) 436 | : `${t.name.replaceAll(':', ' ')} commands` 437 | 438 | return { 439 | description, 440 | name: t.name, 441 | } 442 | }) 443 | 444 | return topics 445 | } 446 | 447 | private sanitizeSummary(summary?: string): string { 448 | if (summary === undefined) { 449 | // PowerShell: 450 | // [System.Management.Automation.CompletionResult] will error out if will error out if you pass in an empty string for the summary. 451 | return ' ' 452 | } 453 | 454 | return ejs 455 | .render(summary, {config: this.config}) 456 | .replaceAll('"', '""') // escape double quotes. 457 | .replaceAll('`', '``') // escape backticks. 458 | .split(EOL)[0] // only use the first line 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/autocomplete/zsh.ts: -------------------------------------------------------------------------------- 1 | import {Command, Config, Interfaces} from '@oclif/core' 2 | import * as ejs from 'ejs' 3 | import {format} from 'node:util' 4 | 5 | const argTemplate = ' "%s")\n %s\n ;;\n' 6 | 7 | type CommandCompletion = { 8 | flags: CommandFlags 9 | id: string 10 | summary: string 11 | } 12 | 13 | type CommandFlags = { 14 | [name: string]: Command.Flag.Cached 15 | } 16 | 17 | type Topic = { 18 | description: string 19 | name: string 20 | } 21 | 22 | export default class ZshCompWithSpaces { 23 | protected config: Config 24 | private _coTopics?: string[] 25 | private commands: CommandCompletion[] 26 | private topics: Topic[] 27 | 28 | constructor(config: Config) { 29 | this.config = config 30 | this.topics = this.getTopics() 31 | this.commands = this.getCommands() 32 | } 33 | 34 | private get coTopics(): string[] { 35 | if (this._coTopics) return this._coTopics 36 | 37 | const coTopics: string[] = [] 38 | 39 | for (const topic of this.topics) { 40 | for (const cmd of this.commands) { 41 | if (topic.name === cmd.id) { 42 | coTopics.push(topic.name) 43 | } 44 | } 45 | } 46 | 47 | this._coTopics = coTopics 48 | 49 | return this._coTopics 50 | } 51 | 52 | public generate(): string { 53 | const firstArgs: {id: string; summary?: string}[] = [] 54 | 55 | for (const t of this.topics) { 56 | if (!t.name.includes(':')) 57 | firstArgs.push({ 58 | id: t.name, 59 | summary: t.description, 60 | }) 61 | } 62 | 63 | for (const c of this.commands) { 64 | if (!firstArgs.some((a) => a.id === c.id) && !c.id.includes(':')) 65 | firstArgs.push({ 66 | id: c.id, 67 | summary: c.summary, 68 | }) 69 | } 70 | 71 | const mainArgsCaseBlock = () => { 72 | let caseBlock = 'case $line[1] in\n' 73 | 74 | for (const arg of firstArgs) { 75 | if (this.coTopics.includes(arg.id)) { 76 | // coTopics already have a completion function. 77 | caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n` 78 | } else { 79 | const cmd = this.commands.find((c) => c.id === arg.id) 80 | 81 | if (cmd) { 82 | // if it's a command and has flags, inline flag completion statement. 83 | // skip it from the args statement if it doesn't accept any flag. 84 | if (Object.keys(cmd.flags).length > 0) { 85 | caseBlock += `${arg.id})\n${this.genZshFlagArgumentsBlock(cmd.flags)} ;; \n` 86 | } 87 | } else { 88 | // it's a topic, redirect to its completion function. 89 | caseBlock += `${arg.id})\n _${this.config.bin}_${arg.id}\n ;;\n` 90 | } 91 | } 92 | } 93 | 94 | caseBlock += 'esac\n' 95 | 96 | return caseBlock 97 | } 98 | 99 | return `#compdef ${this.config.bin} 100 | ${this.config.binAliases?.map((a) => `compdef ${a}=${this.config.bin}`).join('\n') ?? ''} 101 | 102 | ${this.topics.map((t) => this.genZshTopicCompFun(t.name)).join('\n')} 103 | 104 | _${this.config.bin}() { 105 | local context state state_descr line 106 | typeset -A opt_args 107 | 108 | _arguments -C "1: :->cmds" "*::arg:->args" 109 | 110 | case "$state" in 111 | cmds) 112 | ${this.genZshValuesBlock(firstArgs)} 113 | ;; 114 | args) 115 | ${mainArgsCaseBlock()} 116 | ;; 117 | esac 118 | } 119 | 120 | _${this.config.bin} 121 | ` 122 | } 123 | 124 | private genZshFlagArgumentsBlock(flags?: CommandFlags): string { 125 | // if a command doesn't have flags make it only complete files 126 | // also add comp for the global `--help` flag. 127 | if (!flags) return '_arguments -S \\\n --help"[Show help for command]" "*: :_files' 128 | 129 | const flagNames = Object.keys(flags) 130 | 131 | // `-S`: 132 | // Do not complete flags after a ‘--’ appearing on the line, and ignore the ‘--’. For example, with -S, in the line: 133 | // foobar -x -- -y 134 | // the ‘-x’ is considered a flag, the ‘-y’ is considered an argument, and the ‘--’ is considered to be neither. 135 | let argumentsBlock = '_arguments -S \\\n' 136 | 137 | for (const flagName of flagNames) { 138 | const f = flags[flagName] 139 | 140 | // skip hidden flags 141 | if (f.hidden) continue 142 | 143 | const flagSummary = this.sanitizeSummary(f.summary ?? f.description) 144 | 145 | let flagSpec = '' 146 | 147 | if (f.type === 'option') { 148 | if (f.char) { 149 | // eslint-disable-next-line unicorn/prefer-ternary 150 | if (f.multiple) { 151 | // this flag can be present multiple times on the line 152 | flagSpec += `"*"{-${f.char},--${f.name}}` 153 | } else { 154 | flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}` 155 | } 156 | 157 | flagSpec += `"[${flagSummary}]` 158 | 159 | flagSpec += f.options ? `:${f.name} options:(${f.options?.join(' ')})"` : ':file:_files"' 160 | } else { 161 | if (f.multiple) { 162 | // this flag can be present multiple times on the line 163 | flagSpec += '"*"' 164 | } 165 | 166 | flagSpec += `--${f.name}"[${flagSummary}]:` 167 | 168 | flagSpec += f.options ? `${f.name} options:(${f.options.join(' ')})"` : 'file:_files"' 169 | } 170 | } else if (f.char) { 171 | // Flag.Boolean 172 | flagSpec += `"(-${f.char} --${f.name})"{-${f.char},--${f.name}}"[${flagSummary}]"` 173 | } else { 174 | // Flag.Boolean 175 | flagSpec += `--${f.name}"[${flagSummary}]"` 176 | } 177 | 178 | flagSpec += ' \\\n' 179 | argumentsBlock += flagSpec 180 | } 181 | 182 | // add global `--help` flag 183 | argumentsBlock += '--help"[Show help for command]" \\\n' 184 | // complete files if `-` is not present on the current line 185 | argumentsBlock += '"*: :_files"' 186 | 187 | return argumentsBlock 188 | } 189 | 190 | private genZshTopicCompFun(id: string): string { 191 | const coTopics: string[] = [] 192 | 193 | for (const topic of this.topics) { 194 | for (const cmd of this.commands) { 195 | if (topic.name === cmd.id) { 196 | coTopics.push(topic.name) 197 | } 198 | } 199 | } 200 | 201 | const flagArgsTemplate = ' "%s")\n %s\n ;;\n' 202 | 203 | const underscoreSepId = id.replaceAll(':', '_') 204 | const depth = id.split(':').length 205 | 206 | const isCotopic = coTopics.includes(id) 207 | 208 | if (isCotopic) { 209 | const compFuncName = `${this.config.bin}_${underscoreSepId}` 210 | 211 | const coTopicCompFunc = `_${compFuncName}() { 212 | _${compFuncName}_flags() { 213 | local context state state_descr line 214 | typeset -A opt_args 215 | 216 | ${this.genZshFlagArgumentsBlock(this.commands.find((c) => c.id === id)?.flags)} 217 | } 218 | 219 | local context state state_descr line 220 | typeset -A opt_args 221 | 222 | _arguments -C "1: :->cmds" "*: :->args" 223 | 224 | case "$state" in 225 | cmds) 226 | if [[ "\${words[CURRENT]}" == -* ]]; then 227 | _${compFuncName}_flags 228 | else 229 | %s 230 | fi 231 | ;; 232 | args) 233 | case $line[1] in 234 | %s 235 | *) 236 | _${compFuncName}_flags 237 | ;; 238 | esac 239 | ;; 240 | esac 241 | } 242 | ` 243 | const subArgs: {id: string; summary?: string}[] = [] 244 | 245 | let argsBlock = '' 246 | 247 | for (const t of this.topics.filter( 248 | (t) => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1, 249 | )) { 250 | const subArg = t.name.split(':')[depth] 251 | 252 | subArgs.push({ 253 | id: subArg, 254 | summary: t.description, 255 | }) 256 | 257 | argsBlock += format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`) 258 | } 259 | 260 | for (const c of this.commands.filter((c) => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)) { 261 | if (coTopics.includes(c.id)) continue 262 | const subArg = c.id.split(':')[depth] 263 | 264 | subArgs.push({ 265 | id: subArg, 266 | summary: c.summary, 267 | }) 268 | 269 | argsBlock += format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags)) 270 | } 271 | 272 | return format(coTopicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) 273 | } 274 | 275 | let argsBlock = '' 276 | 277 | const subArgs: {id: string; summary?: string}[] = [] 278 | for (const t of this.topics.filter((t) => t.name.startsWith(id + ':') && t.name.split(':').length === depth + 1)) { 279 | const subArg = t.name.split(':')[depth] 280 | 281 | subArgs.push({ 282 | id: subArg, 283 | summary: t.description, 284 | }) 285 | 286 | argsBlock += format(argTemplate, subArg, `_${this.config.bin}_${underscoreSepId}_${subArg}`) 287 | } 288 | 289 | for (const c of this.commands.filter((c) => c.id.startsWith(id + ':') && c.id.split(':').length === depth + 1)) { 290 | if (coTopics.includes(c.id)) continue 291 | const subArg = c.id.split(':')[depth] 292 | 293 | subArgs.push({ 294 | id: subArg, 295 | summary: c.summary, 296 | }) 297 | 298 | argsBlock += format(flagArgsTemplate, subArg, this.genZshFlagArgumentsBlock(c.flags)) 299 | } 300 | 301 | const topicCompFunc = `_${this.config.bin}_${underscoreSepId}() { 302 | local context state state_descr line 303 | typeset -A opt_args 304 | 305 | _arguments -C "1: :->cmds" "*::arg:->args" 306 | 307 | case "$state" in 308 | cmds) 309 | %s 310 | ;; 311 | args) 312 | case $line[1] in 313 | %s 314 | esac 315 | ;; 316 | esac 317 | } 318 | ` 319 | return format(topicCompFunc, this.genZshValuesBlock(subArgs), argsBlock) 320 | } 321 | 322 | private genZshValuesBlock(subArgs: {id: string; summary?: string}[]): string { 323 | let valuesBlock = '_values "completions" \\\n' 324 | 325 | for (const subArg of subArgs) { 326 | valuesBlock += `"${subArg.id}[${subArg.summary}]" \\\n` 327 | } 328 | 329 | return valuesBlock 330 | } 331 | 332 | private getCommands(): CommandCompletion[] { 333 | const cmds: CommandCompletion[] = [] 334 | 335 | for (const p of this.config.getPluginsList()) { 336 | for (const c of p.commands) { 337 | if (c.hidden) continue 338 | const summary = this.sanitizeSummary(c.summary ?? c.description) 339 | const {flags} = c 340 | cmds.push({ 341 | flags, 342 | id: c.id, 343 | summary, 344 | }) 345 | 346 | for (const a of c.aliases) { 347 | cmds.push({ 348 | flags, 349 | id: a, 350 | summary, 351 | }) 352 | 353 | const split = a.split(':') 354 | 355 | let topic = split[0] 356 | 357 | // Completion funcs are generated from topics: 358 | // `force` -> `force:org` -> `force:org:open|list` 359 | // 360 | // but aliases aren't guaranteed to follow the plugin command tree 361 | // so we need to add any missing topic between the starting point and the alias. 362 | for (let i = 0; i < split.length - 1; i++) { 363 | if (!this.topics.some((t) => t.name === topic)) { 364 | this.topics.push({ 365 | description: `${topic.replaceAll(':', ' ')} commands`, 366 | name: topic, 367 | }) 368 | } 369 | 370 | topic += `:${split[i + 1]}` 371 | } 372 | } 373 | } 374 | } 375 | 376 | return cmds 377 | } 378 | 379 | private getTopics(): Topic[] { 380 | const topics = this.config.topics 381 | .filter((topic: Interfaces.Topic) => { 382 | // it is assumed a topic has a child if it has children 383 | const hasChild = this.config.topics.some((subTopic) => subTopic.name.includes(`${topic.name}:`)) 384 | return hasChild 385 | }) 386 | .sort((a, b) => { 387 | if (a.name < b.name) { 388 | return -1 389 | } 390 | 391 | if (a.name > b.name) { 392 | return 1 393 | } 394 | 395 | return 0 396 | }) 397 | .map((t) => { 398 | const description = t.description 399 | ? this.sanitizeSummary(t.description) 400 | : `${t.name.replaceAll(':', ' ')} commands` 401 | 402 | return { 403 | description, 404 | name: t.name, 405 | } 406 | }) 407 | 408 | return topics 409 | } 410 | 411 | private sanitizeSummary(summary?: string): string { 412 | if (summary === undefined) { 413 | return '' 414 | } 415 | 416 | return ejs 417 | .render(summary, {config: this.config}) 418 | .replaceAll(/(["`])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes 419 | 420 | .replaceAll(/([[\]])/g, '\\\\$1') // square brackets require double-backslashes 421 | .split('\n')[0] // only use the first line 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import {Command} from '@oclif/core' 2 | import {mkdirSync, openSync, writeSync} from 'node:fs' 3 | import path from 'node:path' 4 | 5 | export abstract class AutocompleteBase extends Command { 6 | public get acLogfilePath(): string { 7 | return path.join(this.config.cacheDir, 'autocomplete.log') 8 | } 9 | 10 | public get autocompleteCacheDir(): string { 11 | return path.join(this.config.cacheDir, 'autocomplete') 12 | } 13 | 14 | public get cliBin() { 15 | return this.config.bin 16 | } 17 | 18 | public get cliBinEnvVar() { 19 | return this.config.bin.toUpperCase().replaceAll('-', '_') 20 | } 21 | 22 | public determineShell(shell: string) { 23 | if (!shell) { 24 | this.error('Missing required argument shell') 25 | } else if (this.isBashOnWindows(shell)) { 26 | return 'bash' 27 | } else { 28 | return shell 29 | } 30 | } 31 | 32 | public getSetupEnvVar(shell: string): string { 33 | return `${this.cliBinEnvVar}_AC_${shell.toUpperCase()}_SETUP_PATH` 34 | } 35 | 36 | writeLogFile(msg: string) { 37 | mkdirSync(this.config.cacheDir, {recursive: true}) 38 | const entry = `[${new Date().toISOString()}] ${msg}\n` 39 | const fd = openSync(this.acLogfilePath, 'a') 40 | writeSync(fd, entry) 41 | } 42 | 43 | private isBashOnWindows(shell: string) { 44 | return shell.endsWith('\\bash.exe') 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/autocomplete/create.ts: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug' 2 | import {mkdir, writeFile} from 'node:fs/promises' 3 | import path from 'node:path' 4 | 5 | import bashAutocompleteWithSpaces from '../../autocomplete/bash-spaces.js' 6 | import bashAutocomplete from '../../autocomplete/bash.js' 7 | import PowerShellComp from '../../autocomplete/powershell.js' 8 | import ZshCompWithSpaces from '../../autocomplete/zsh.js' 9 | import {AutocompleteBase} from '../../base.js' 10 | 11 | const debug = makeDebug('autocomplete:create') 12 | 13 | type CommandCompletion = { 14 | description: string 15 | flags?: any 16 | id: string 17 | } 18 | 19 | function sanitizeDescription(description?: string): string { 20 | if (description === undefined) { 21 | return '' 22 | } 23 | 24 | return description 25 | .replaceAll(/(["`])/g, '\\\\\\$1') // backticks and double-quotes require triple-backslashes 26 | 27 | .replaceAll(/([[\]])/g, '\\\\$1') // square brackets require double-backslashes 28 | .split('\n')[0] // only use the first line 29 | } 30 | 31 | export default class Create extends AutocompleteBase { 32 | static description = 'create autocomplete setup scripts and completion functions' 33 | static hidden = true 34 | private _commands?: CommandCompletion[] 35 | 36 | private get bashCommandsWithFlagsList(): string { 37 | return this.commands 38 | .map((c) => { 39 | const publicFlags = this.genCmdPublicFlags(c).trim() 40 | return `${c.id} ${publicFlags}` 41 | }) 42 | .join('\n') 43 | } 44 | 45 | private get bashCompletionFunction(): string { 46 | const {cliBin} = this 47 | const supportSpaces = this.config.topicSeparator === ' ' 48 | const bashScript = 49 | process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' || !supportSpaces 50 | ? bashAutocomplete 51 | : bashAutocompleteWithSpaces 52 | return ( 53 | bashScript 54 | // eslint-disable-next-line unicorn/prefer-spread 55 | .concat( 56 | ...(this.config.binAliases?.map((alias) => `complete -F __autocomplete ${alias}`).join('\n') ?? []), 57 | ) 58 | .replaceAll('', cliBin) 59 | .replaceAll('', this.bashCommandsWithFlagsList) 60 | ) 61 | } 62 | 63 | private get bashCompletionFunctionPath(): string { 64 | // /autocomplete/functions/bash/.bash 65 | return path.join(this.bashFunctionsDir, `${this.cliBin}.bash`) 66 | } 67 | 68 | private get bashFunctionsDir(): string { 69 | // /autocomplete/functions/bash 70 | return path.join(this.autocompleteCacheDir, 'functions', 'bash') 71 | } 72 | 73 | private get bashSetupScript(): string { 74 | const setup = path.join(this.bashFunctionsDir, `${this.cliBin}.bash`) 75 | const bin = this.cliBinEnvVar 76 | /* eslint-disable-next-line no-useless-escape */ 77 | return `${bin}_AC_BASH_COMPFUNC_PATH=${setup} && test -f \$${bin}_AC_BASH_COMPFUNC_PATH && source \$${bin}_AC_BASH_COMPFUNC_PATH; 78 | ` 79 | } 80 | 81 | private get bashSetupScriptPath(): string { 82 | // /autocomplete/bash_setup 83 | return path.join(this.autocompleteCacheDir, 'bash_setup') 84 | } 85 | 86 | private get commands(): CommandCompletion[] { 87 | if (this._commands) return this._commands 88 | 89 | const cmds: CommandCompletion[] = [] 90 | 91 | for (const p of this.config.getPluginsList()) { 92 | for (const c of p.commands) { 93 | try { 94 | if (c.hidden) continue 95 | const description = sanitizeDescription(c.summary ?? (c.description || '')) 96 | const {flags} = c 97 | cmds.push({ 98 | description, 99 | flags, 100 | id: c.id, 101 | }) 102 | for (const a of c.aliases) { 103 | cmds.push({ 104 | description, 105 | flags, 106 | id: a, 107 | }) 108 | } 109 | } catch (error: any) { 110 | debug(`Error creating zsh flag spec for command ${c.id}`) 111 | debug(error.message) 112 | this.writeLogFile(error.message) 113 | } 114 | } 115 | } 116 | 117 | this._commands = cmds 118 | 119 | return this._commands 120 | } 121 | 122 | private get genAllCommandsMetaString(): string { 123 | // eslint-disable-next-line no-useless-escape 124 | return this.commands.map((c) => `\"${c.id.replaceAll(':', '\\:')}:${c.description}\"`).join('\n') 125 | } 126 | 127 | private get genCaseStatementForFlagsMetaString(): string { 128 | // command) 129 | // _command_flags=( 130 | // "--boolean[bool descr]" 131 | // "--value=-[value descr]:" 132 | // ) 133 | // ;; 134 | return this.commands 135 | .map( 136 | (c) => `${c.id}) 137 | _command_flags=( 138 | ${this.genZshFlagSpecs(c)} 139 | ) 140 | ;;\n`, 141 | ) 142 | .join('\n') 143 | } 144 | 145 | private get pwshCompletionFunctionPath(): string { 146 | // /autocomplete/functions/powershell/.ps1 147 | return path.join(this.pwshFunctionsDir, `${this.cliBin}.ps1`) 148 | } 149 | 150 | private get pwshFunctionsDir(): string { 151 | // /autocomplete/functions/powershell 152 | return path.join(this.autocompleteCacheDir, 'functions', 'powershell') 153 | } 154 | 155 | private get zshCompletionFunction(): string { 156 | const {cliBin} = this 157 | const allCommandsMeta = this.genAllCommandsMetaString 158 | const caseStatementForFlagsMeta = this.genCaseStatementForFlagsMetaString 159 | 160 | return `#compdef ${cliBin} 161 | 162 | _${cliBin} () { 163 | local _command_id=\${words[2]} 164 | local _cur=\${words[CURRENT]} 165 | local -a _command_flags=() 166 | 167 | ## public cli commands & flags 168 | local -a _all_commands=( 169 | ${allCommandsMeta} 170 | ) 171 | 172 | _set_flags () { 173 | case $_command_id in 174 | ${caseStatementForFlagsMeta} 175 | esac 176 | } 177 | ## end public cli commands & flags 178 | 179 | _complete_commands () { 180 | _describe -t all-commands "all commands" _all_commands 181 | } 182 | 183 | if [ $CURRENT -gt 2 ]; then 184 | if [[ "$_cur" == -* ]]; then 185 | _set_flags 186 | else 187 | _path_files 188 | fi 189 | fi 190 | 191 | 192 | _arguments -S '1: :_complete_commands' \\ 193 | $_command_flags 194 | } 195 | 196 | _${cliBin} 197 | ` 198 | } 199 | 200 | private get zshCompletionFunctionPath(): string { 201 | // /autocomplete/functions/zsh/_ 202 | return path.join(this.zshFunctionsDir, `_${this.cliBin}`) 203 | } 204 | 205 | private get zshFunctionsDir(): string { 206 | // /autocomplete/functions/zsh 207 | return path.join(this.autocompleteCacheDir, 'functions', 'zsh') 208 | } 209 | 210 | private get zshSetupScript(): string { 211 | return ` 212 | fpath=( 213 | ${this.zshFunctionsDir} 214 | $fpath 215 | ); 216 | autoload -Uz compinit; 217 | compinit;\n` 218 | } 219 | 220 | private get zshSetupScriptPath(): string { 221 | // /autocomplete/zsh_setup 222 | return path.join(this.autocompleteCacheDir, 'zsh_setup') 223 | } 224 | 225 | async run() { 226 | // 1. ensure needed dirs 227 | await this.ensureDirs() 228 | // 2. save (generated) autocomplete files 229 | await this.createFiles() 230 | } 231 | 232 | private async createFiles() { 233 | // zsh 234 | const supportSpaces = this.config.topicSeparator === ' ' 235 | 236 | await Promise.all( 237 | [ 238 | writeFile(this.bashSetupScriptPath, this.bashSetupScript), 239 | writeFile(this.bashCompletionFunctionPath, this.bashCompletionFunction), 240 | writeFile(this.zshSetupScriptPath, this.zshSetupScript), 241 | // eslint-disable-next-line unicorn/prefer-spread 242 | ].concat( 243 | process.env.OCLIF_AUTOCOMPLETE_TOPIC_SEPARATOR === 'colon' || !supportSpaces 244 | ? [writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction)] 245 | : [ 246 | writeFile(this.zshCompletionFunctionPath, new ZshCompWithSpaces(this.config).generate()), 247 | writeFile(this.pwshCompletionFunctionPath, new PowerShellComp(this.config).generate()), 248 | ], 249 | ), 250 | ) 251 | } 252 | 253 | private async ensureDirs() { 254 | // ensure autocomplete cache dir before doing the children 255 | await mkdir(this.autocompleteCacheDir, {recursive: true}) 256 | await Promise.all([ 257 | mkdir(this.bashFunctionsDir, {recursive: true}), 258 | mkdir(this.zshFunctionsDir, {recursive: true}), 259 | mkdir(this.pwshFunctionsDir, {recursive: true}), 260 | ]) 261 | } 262 | 263 | private genCmdPublicFlags(Command: CommandCompletion): string { 264 | const Flags = Command.flags || {} 265 | return Object.keys(Flags) 266 | .filter((flag) => !Flags[flag].hidden) 267 | .map((flag) => `--${flag}`) 268 | .join(' ') 269 | } 270 | 271 | private genZshFlagSpecs(Klass: any): string { 272 | return Object.keys(Klass.flags || {}) 273 | .filter((flag) => Klass.flags && !Klass.flags[flag].hidden) 274 | .map((flag) => { 275 | const f = (Klass.flags && Klass.flags[flag]) || {description: ''} 276 | const isBoolean = f.type === 'boolean' 277 | const isOption = f.type === 'option' 278 | const name = isBoolean ? flag : `${flag}=-` 279 | const multiple = isOption && f.multiple ? '*' : '' 280 | const valueCmpl = isBoolean ? '' : ':' 281 | const completion = `${multiple}--${name}[${sanitizeDescription(f.summary || f.description)}]${valueCmpl}` 282 | return `"${completion}"` 283 | }) 284 | .join('\n') 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/commands/autocomplete/index.ts: -------------------------------------------------------------------------------- 1 | import {Args, Flags, ux} from '@oclif/core' 2 | import {bold, cyan} from 'ansis' 3 | import {EOL} from 'node:os' 4 | 5 | import {AutocompleteBase} from '../../base.js' 6 | import Create from './create.js' 7 | 8 | export default class Index extends AutocompleteBase { 9 | static args = { 10 | shell: Args.string({ 11 | description: 'Shell type', 12 | options: ['zsh', 'bash', 'powershell'], 13 | required: false, 14 | }), 15 | } 16 | static description = 'Display autocomplete installation instructions.' 17 | static examples = [ 18 | '$ <%= config.bin %> autocomplete', 19 | '$ <%= config.bin %> autocomplete bash', 20 | '$ <%= config.bin %> autocomplete zsh', 21 | '$ <%= config.bin %> autocomplete powershell', 22 | '$ <%= config.bin %> autocomplete --refresh-cache', 23 | ] 24 | static flags = { 25 | 'refresh-cache': Flags.boolean({char: 'r', description: 'Refresh cache (ignores displaying instructions)'}), 26 | } 27 | 28 | async run() { 29 | const {args, flags} = await this.parse(Index) 30 | const shell = args.shell ?? this.determineShell(this.config.shell) 31 | 32 | if (shell === 'powershell' && this.config?.topicSeparator === ':') { 33 | this.error( 34 | `PowerShell completion is not supported in CLIs using colon as the topic separator.${EOL}See: https://oclif.io/docs/topic_separator`, 35 | ) 36 | } 37 | 38 | ux.action.start(`${bold('Building the autocomplete cache')}`) 39 | await Create.run([], this.config) 40 | ux.action.stop() 41 | 42 | if (!flags['refresh-cache']) { 43 | this.printShellInstructions(shell) 44 | } 45 | } 46 | 47 | private printShellInstructions(shell: string): void { 48 | const setupEnvVar = this.getSetupEnvVar(shell) 49 | const tabStr = shell === 'bash' ? '' : '' 50 | const scriptCommand = `${this.config.bin} autocomplete${this.config.topicSeparator}script ${shell}` 51 | 52 | let instructions = ` 53 | Setup Instructions for ${this.config.bin.toUpperCase()} CLI Autocomplete --- 54 | ============================================== 55 | ` 56 | 57 | switch (shell) { 58 | case 'bash': { 59 | instructions += ` 60 | 1) Run this command in your terminal window: 61 | 62 | ${cyan(`printf "$(${scriptCommand})" >> ~/.bashrc; source ~/.bashrc`)} 63 | 64 | The previous command adds the ${cyan(setupEnvVar)} environment variable to your Bash config file and then sources the file. 65 | 66 | ${bold('NOTE')}: If you’ve configured your terminal to start as a login shell, you may need to modify the command so it updates either the ~/.bash_profile or ~/.profile file. For example: 67 | 68 | ${cyan(`printf "$(${scriptCommand})" >> ~/.bash_profile; source ~/.bash_profile`)} 69 | 70 | Or: 71 | 72 | ${cyan(`printf "$(${scriptCommand})" >> ~/.profile; source ~/.profile`)} 73 | 74 | 2) Start using autocomplete: 75 | 76 | ${cyan(`${this.config.bin} ${tabStr}`)} # Command completion 77 | ${cyan(`${this.config.bin} command --${tabStr}`)} # Flag completion 78 | ` 79 | break 80 | } 81 | 82 | case 'powershell': { 83 | instructions += ` 84 | 1) Run these two cmdlets in your PowerShell window in the order shown: 85 | 86 | ${cyan(`New-Item -Type Directory -Path (Split-Path -Parent $PROFILE) -ErrorAction SilentlyContinue 87 | Add-Content -Path $PROFILE -Value (Invoke-Expression -Command "${scriptCommand}"); .$PROFILE`)} 88 | 89 | 2) (Optional) If you want matching completions printed below the command line, run this cmdlet: 90 | 91 | ${cyan('Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete')} 92 | 93 | 3) Start using autocomplete: 94 | 95 | ${cyan(`${this.config.bin} ${tabStr}`)} # Command completion 96 | ${cyan(`${this.config.bin} command --${tabStr}`)} # Flag completion 97 | ` 98 | break 99 | } 100 | 101 | case 'zsh': { 102 | instructions += ` 103 | 1) Run this command in your terminal window: 104 | 105 | ${cyan(`printf "$(${scriptCommand})" >> ~/.zshrc; source ~/.zshrc`)} 106 | 107 | The previous command adds the ${cyan(setupEnvVar)} environment variable to your zsh config file and then sources the file. 108 | 109 | 2) (Optional) Run this command to ensure that you have no permissions conflicts: 110 | 111 | ${cyan('compaudit -D')} 112 | 113 | 3) Start using autocomplete: 114 | 115 | ${cyan(`${this.config.bin} ${tabStr}`)} # Command completion 116 | ${cyan(`${this.config.bin} command --${tabStr}`)} # Flag completion 117 | ` 118 | break 119 | } 120 | } 121 | 122 | instructions += ` 123 | Every time you enter ${tabStr}, the autocomplete feature displays a list of commands (or flags if you type --), along with their summaries. Enter a letter and then ${tabStr} again to narrow down the list until you end up with the complete command that you want to execute. 124 | 125 | Enjoy! 126 | ` 127 | this.log(instructions) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/commands/autocomplete/script.ts: -------------------------------------------------------------------------------- 1 | import {Args} from '@oclif/core' 2 | import path from 'node:path' 3 | 4 | import {AutocompleteBase} from '../../base.js' 5 | 6 | export default class Script extends AutocompleteBase { 7 | static args = { 8 | shell: Args.string({ 9 | description: 'Shell type', 10 | options: ['zsh', 'bash', 'powershell'], 11 | required: false, 12 | }), 13 | } 14 | static description = 'outputs autocomplete config script for shells' 15 | static hidden = true 16 | 17 | private get prefix(): string { 18 | return '\n' 19 | } 20 | 21 | private get suffix(): string { 22 | return ` # ${this.cliBin} autocomplete setup\n` 23 | } 24 | 25 | async run() { 26 | const {args} = await this.parse(Script) 27 | const shell = args.shell ?? this.config.shell 28 | 29 | if (shell === 'powershell') { 30 | const completionFuncPath = path.join( 31 | this.config.cacheDir, 32 | 'autocomplete', 33 | 'functions', 34 | 'powershell', 35 | `${this.cliBin}.ps1`, 36 | ) 37 | this.log(`. ${completionFuncPath}`) 38 | } else { 39 | this.log( 40 | `${this.prefix}${this.getSetupEnvVar(shell)}=${path.join( 41 | this.autocompleteCacheDir, 42 | `${shell}_setup`, 43 | )} && test -f $${this.getSetupEnvVar(shell)} && source $${this.getSetupEnvVar(shell)};${this.suffix}`, 44 | ) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/hooks/refresh-cache.ts: -------------------------------------------------------------------------------- 1 | import {Hook} from '@oclif/core' 2 | 3 | const hook: Hook<'refresh'> = async function (opts) { 4 | // this `config` instance already have installed/uninstalled plugins loaded 5 | await opts.config.runCommand('autocomplete:create') 6 | } 7 | 8 | export default hook 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /test/autocomplete/bash.test.ts: -------------------------------------------------------------------------------- 1 | import {Command, Config} from '@oclif/core' 2 | import {Plugin as IPlugin} from '@oclif/core/interfaces' 3 | import {expect} from 'chai' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | import Create from '../../src/commands/autocomplete/create.js' 8 | // autocomplete will throw error on windows ci 9 | import {default as skipWindows} from '../helpers/runtest.js' 10 | 11 | class MyCommandClass implements Command.Cached { 12 | [key: string]: unknown 13 | 14 | _base = '' 15 | aliases: string[] = [] 16 | args: {[name: string]: Command.Arg.Cached} = {} 17 | flags = {} 18 | hidden = false 19 | hiddenAliases!: string[] 20 | id = 'foo:bar' 21 | 22 | new(): Command.Cached { 23 | // @ts-expect-error this is not the full interface but enough for testing 24 | return { 25 | _run(): Promise { 26 | return Promise.resolve() 27 | }, 28 | } 29 | } 30 | 31 | run(): PromiseLike { 32 | return Promise.resolve() 33 | } 34 | } 35 | 36 | const commandPluginA: Command.Loadable = { 37 | aliases: [], 38 | args: {}, 39 | flags: { 40 | 'api-version': { 41 | char: 'a', 42 | multiple: false, 43 | name: 'api-version', 44 | type: 'option', 45 | }, 46 | 'ignore-errors': { 47 | allowNo: false, 48 | char: 'i', 49 | name: 'ignore-errors', 50 | summary: 'Ignore errors.', 51 | type: 'boolean', 52 | }, 53 | json: { 54 | allowNo: false, 55 | name: 'json', 56 | summary: 'Format output as json.', 57 | type: 'boolean', 58 | }, 59 | metadata: { 60 | char: 'm', 61 | multiple: true, 62 | name: 'metadata', 63 | type: 'option', 64 | }, 65 | }, 66 | hidden: false, 67 | hiddenAliases: [], 68 | id: 'deploy', 69 | async load(): Promise { 70 | return new MyCommandClass() as unknown as Command.Class 71 | }, 72 | pluginAlias: '@My/plugina', 73 | pluginType: 'core', 74 | strict: false, 75 | summary: 'Deploy a project', 76 | } 77 | 78 | const commandPluginB: Command.Loadable = { 79 | aliases: [], 80 | args: {}, 81 | flags: { 82 | branch: { 83 | char: 'b', 84 | multiple: false, 85 | name: 'branch', 86 | type: 'option', 87 | }, 88 | }, 89 | hidden: false, 90 | hiddenAliases: [], 91 | id: 'deploy:functions', 92 | async load(): Promise { 93 | return new MyCommandClass() as unknown as Command.Class 94 | }, 95 | pluginAlias: '@My/pluginb', 96 | pluginType: 'core', 97 | strict: false, 98 | summary: 'Deploy a function.', 99 | } 100 | 101 | const commandPluginC: Command.Loadable = { 102 | aliases: [], 103 | args: {}, 104 | flags: {}, 105 | hidden: false, 106 | hiddenAliases: [], 107 | id: 'search', 108 | async load(): Promise { 109 | return new MyCommandClass() as unknown as Command.Class 110 | }, 111 | pluginAlias: '@My/pluginc', 112 | pluginType: 'core', 113 | strict: false, 114 | summary: 'Search for a command', 115 | } 116 | 117 | const commandPluginD: Command.Loadable = { 118 | aliases: [], 119 | args: {}, 120 | flags: {}, 121 | hidden: false, 122 | hiddenAliases: [], 123 | id: 'app:execute:code', 124 | async load(): Promise { 125 | return new MyCommandClass() as unknown as Command.Class 126 | }, 127 | pluginAlias: '@My/plugind', 128 | pluginType: 'core', 129 | strict: false, 130 | summary: 'execute code', 131 | } 132 | 133 | const pluginA: IPlugin = { 134 | _base: '', 135 | alias: '@My/plugina', 136 | commandIDs: ['deploy'], 137 | commands: [commandPluginA, commandPluginB, commandPluginC, commandPluginD], 138 | commandsDir: '', 139 | async findCommand(): Promise { 140 | return new MyCommandClass() as unknown as Command.Class 141 | }, 142 | hasManifest: true, 143 | hooks: {}, 144 | isRoot: false, 145 | async load(): Promise {}, 146 | moduleType: 'commonjs', 147 | name: '@My/plugina', 148 | options: {root: ''}, 149 | pjson: {} as any, 150 | root: '', 151 | tag: 'tag', 152 | topics: [ 153 | { 154 | description: 'foo commands', 155 | name: 'foo', 156 | }, 157 | ], 158 | type: 'core', 159 | valid: true, 160 | version: '0.0.0', 161 | } 162 | 163 | const plugins: IPlugin[] = [pluginA] 164 | 165 | skipWindows('bash comp', () => { 166 | describe('bash completion', () => { 167 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json') 168 | const config = new Config({root}) 169 | 170 | before(async () => { 171 | await config.load() 172 | for (const plugin of plugins) config.plugins.set(plugin.name, plugin) 173 | config.pjson.oclif.plugins = ['@My/pluginb'] 174 | config.pjson.dependencies = {'@My/pluginb': '0.0.0'} 175 | for (const plugin of config.getPluginsList()) { 176 | // @ts-expect-error private method 177 | config.loadCommands(plugin) 178 | // @ts-expect-error private method 179 | config.loadTopics(plugin) 180 | } 181 | }) 182 | 183 | it('generates a valid completion file.', async () => { 184 | config.bin = 'test-cli' 185 | const create = new Create([], config) 186 | 187 | // @ts-expect-error because it's a private method 188 | expect(create.bashCompletionFunction.trim()).to.equal(`#!/usr/bin/env bash 189 | 190 | _test-cli_autocomplete() 191 | { 192 | 193 | local cur="$\{COMP_WORDS[COMP_CWORD]}" opts IFS=$' \\t\\n' 194 | COMPREPLY=() 195 | 196 | local commands=" 197 | autocomplete --refresh-cache 198 | deploy --api-version --ignore-errors --json --metadata 199 | deploy:functions --branch 200 | ${'search '} 201 | ${'app:execute:code '} 202 | " 203 | 204 | if [[ "$cur" != "-"* ]]; then 205 | opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+') 206 | else 207 | local __COMP_WORDS 208 | if [[ $\{COMP_WORDS[2]} == ":" ]]; then 209 | #subcommand 210 | __COMP_WORDS=$(printf "%s" "$\{COMP_WORDS[@]:1:3}") 211 | else 212 | #simple command 213 | __COMP_WORDS="$\{COMP_WORDS[@]:1:1}" 214 | fi 215 | opts=$(printf "$commands" | grep "$\{__COMP_WORDS}" | sed -n "s/^$\{__COMP_WORDS} //p") 216 | fi 217 | _get_comp_words_by_ref -n : cur 218 | COMPREPLY=( $(compgen -W "$\{opts}" -- $\{cur}) ) 219 | __ltrim_colon_completions "$cur" 220 | return 0 221 | 222 | } 223 | 224 | complete -o default -F _test-cli_autocomplete test-cli`) 225 | }) 226 | 227 | it('generates a valid completion file with an alias.', async () => { 228 | config.bin = 'test-cli' 229 | config.binAliases = ['alias'] 230 | const create = new Create([], config) 231 | // @ts-expect-error because it's a private method 232 | expect(create.bashCompletionFunction.trim()).to.equal(`#!/usr/bin/env bash 233 | 234 | _test-cli_autocomplete() 235 | { 236 | 237 | local cur="$\{COMP_WORDS[COMP_CWORD]}" opts IFS=$' \\t\\n' 238 | COMPREPLY=() 239 | 240 | local commands=" 241 | autocomplete --refresh-cache 242 | deploy --api-version --ignore-errors --json --metadata 243 | deploy:functions --branch 244 | ${'search '} 245 | ${'app:execute:code '} 246 | " 247 | 248 | if [[ "$cur" != "-"* ]]; then 249 | opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+') 250 | else 251 | local __COMP_WORDS 252 | if [[ $\{COMP_WORDS[2]} == ":" ]]; then 253 | #subcommand 254 | __COMP_WORDS=$(printf "%s" "$\{COMP_WORDS[@]:1:3}") 255 | else 256 | #simple command 257 | __COMP_WORDS="$\{COMP_WORDS[@]:1:1}" 258 | fi 259 | opts=$(printf "$commands" | grep "$\{__COMP_WORDS}" | sed -n "s/^$\{__COMP_WORDS} //p") 260 | fi 261 | _get_comp_words_by_ref -n : cur 262 | COMPREPLY=( $(compgen -W "$\{opts}" -- $\{cur}) ) 263 | __ltrim_colon_completions "$cur" 264 | return 0 265 | 266 | } 267 | 268 | complete -o default -F _test-cli_autocomplete test-cli 269 | complete -F _test-cli_autocomplete alias`) 270 | }) 271 | 272 | it('generates a valid completion file with multiple aliases.', async () => { 273 | config.bin = 'test-cli' 274 | config.binAliases = ['alias', 'alias2'] 275 | const create = new Create([], config) 276 | // @ts-expect-error because it's a private method 277 | expect(create.bashCompletionFunction).to.equal(`#!/usr/bin/env bash 278 | 279 | _test-cli_autocomplete() 280 | { 281 | 282 | local cur="$\{COMP_WORDS[COMP_CWORD]}" opts IFS=$' \\t\\n' 283 | COMPREPLY=() 284 | 285 | local commands=" 286 | autocomplete --refresh-cache 287 | deploy --api-version --ignore-errors --json --metadata 288 | deploy:functions --branch 289 | ${'search '} 290 | ${'app:execute:code '} 291 | " 292 | 293 | if [[ "$cur" != "-"* ]]; then 294 | opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+') 295 | else 296 | local __COMP_WORDS 297 | if [[ $\{COMP_WORDS[2]} == ":" ]]; then 298 | #subcommand 299 | __COMP_WORDS=$(printf "%s" "$\{COMP_WORDS[@]:1:3}") 300 | else 301 | #simple command 302 | __COMP_WORDS="$\{COMP_WORDS[@]:1:1}" 303 | fi 304 | opts=$(printf "$commands" | grep "$\{__COMP_WORDS}" | sed -n "s/^$\{__COMP_WORDS} //p") 305 | fi 306 | _get_comp_words_by_ref -n : cur 307 | COMPREPLY=( $(compgen -W "$\{opts}" -- $\{cur}) ) 308 | __ltrim_colon_completions "$cur" 309 | return 0 310 | 311 | } 312 | 313 | complete -o default -F _test-cli_autocomplete test-cli 314 | complete -F _test-cli_autocomplete alias 315 | complete -F _test-cli_autocomplete alias2`) 316 | }) 317 | }) 318 | }) 319 | -------------------------------------------------------------------------------- /test/autocomplete/powershell.test.ts: -------------------------------------------------------------------------------- 1 | import {Command, Config} from '@oclif/core' 2 | import {Deprecation, Plugin as IPlugin} from '@oclif/core/interfaces' 3 | import {expect} from 'chai' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | import PowerShellComp from '../../src/autocomplete/powershell.js' 8 | 9 | class MyCommandClass implements Command.Cached { 10 | [key: string]: unknown 11 | _base = '' 12 | aliases: string[] = [] 13 | aliasPermutations?: string[] | undefined 14 | args: {[name: string]: Command.Arg.Cached} = {} 15 | deprecateAliases?: boolean | undefined 16 | deprecationOptions?: Deprecation | undefined 17 | description?: string | undefined 18 | examples?: Command.Example[] | undefined 19 | flags = {} 20 | hasDynamicHelp?: boolean | undefined 21 | hidden = false 22 | hiddenAliases!: string[] 23 | id = 'foo:bar' 24 | isESM?: boolean | undefined 25 | permutations?: string[] | undefined 26 | pluginAlias?: string | undefined 27 | pluginName?: string | undefined 28 | pluginType?: string | undefined 29 | relativePath?: string[] | undefined 30 | state?: string | undefined 31 | strict?: boolean | undefined 32 | summary?: string | undefined 33 | type?: string | undefined 34 | usage?: string | string[] | undefined 35 | 36 | new(): Command.Cached { 37 | // @ts-expect-error this is not the full interface but enough for testing 38 | return { 39 | _run(): Promise { 40 | return Promise.resolve() 41 | }, 42 | } 43 | } 44 | 45 | run(): PromiseLike { 46 | return Promise.resolve() 47 | } 48 | } 49 | 50 | const commandPluginA: Command.Loadable = { 51 | aliases: [], 52 | args: {}, 53 | flags: { 54 | 'api-version': { 55 | char: 'a', 56 | multiple: false, 57 | name: 'api-version', 58 | type: 'option', 59 | }, 60 | 'ignore-errors': { 61 | allowNo: false, 62 | char: 'i', 63 | name: 'ignore-errors', 64 | summary: 'Ignore errors.', 65 | type: 'boolean', 66 | }, 67 | json: { 68 | allowNo: false, 69 | name: 'json', 70 | summary: 'Format output as "json".', 71 | type: 'boolean', 72 | }, 73 | metadata: { 74 | char: 'm', 75 | multiple: true, 76 | name: 'metadata', 77 | type: 'option', 78 | }, 79 | }, 80 | hidden: false, 81 | hiddenAliases: [], 82 | id: 'deploy', 83 | async load(): Promise { 84 | return new MyCommandClass() as unknown as Command.Class 85 | }, 86 | pluginAlias: '@My/plugina', 87 | pluginType: 'core', 88 | strict: false, 89 | summary: 'Deploy a project', 90 | } 91 | 92 | const commandPluginB: Command.Loadable = { 93 | aliases: [], 94 | args: {}, 95 | flags: { 96 | branch: { 97 | char: 'b', 98 | multiple: false, 99 | name: 'branch', 100 | type: 'option', 101 | }, 102 | }, 103 | hidden: false, 104 | hiddenAliases: [], 105 | id: 'deploy:functions', 106 | async load(): Promise { 107 | return new MyCommandClass() as unknown as Command.Class 108 | }, 109 | pluginAlias: '@My/pluginb', 110 | pluginType: 'core', 111 | strict: false, 112 | summary: 'Deploy a function.', 113 | } 114 | 115 | const commandPluginC: Command.Loadable = { 116 | aliases: [], 117 | args: {}, 118 | flags: {}, 119 | hidden: false, 120 | hiddenAliases: [], 121 | id: 'search', 122 | async load(): Promise { 123 | return new MyCommandClass() as unknown as Command.Class 124 | }, 125 | pluginAlias: '@My/pluginc', 126 | pluginType: 'core', 127 | strict: false, 128 | summary: 'Search for a command', 129 | } 130 | 131 | const commandPluginD: Command.Loadable = { 132 | aliases: [], 133 | args: {}, 134 | flags: {}, 135 | hidden: false, 136 | hiddenAliases: [], 137 | id: 'app:execute:code', 138 | async load(): Promise { 139 | return new MyCommandClass() as unknown as Command.Class 140 | }, 141 | pluginAlias: '@My/plugind', 142 | pluginType: 'core', 143 | strict: false, 144 | summary: 'execute code', 145 | } 146 | 147 | const pluginA: IPlugin = { 148 | _base: '', 149 | alias: '@My/plugina', 150 | commandIDs: ['deploy'], 151 | commands: [commandPluginA, commandPluginB, commandPluginC, commandPluginD], 152 | commandsDir: '', 153 | async findCommand(): Promise { 154 | return new MyCommandClass() as unknown as Command.Class 155 | }, 156 | hasManifest: false, 157 | hooks: {}, 158 | isRoot: false, 159 | async load(): Promise {}, 160 | moduleType: 'commonjs', 161 | name: '@My/plugina', 162 | options: {root: ''}, 163 | pjson: {} as any, 164 | root: '', 165 | tag: 'tag', 166 | topics: [ 167 | { 168 | description: 'foo commands', 169 | name: 'foo', 170 | }, 171 | ], 172 | type: 'core', 173 | valid: true, 174 | version: '0.0.0', 175 | } 176 | 177 | const plugins: IPlugin[] = [pluginA] 178 | 179 | describe('powershell completion', () => { 180 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json') 181 | const config = new Config({root}) 182 | 183 | before(async () => { 184 | await config.load() 185 | 186 | for (const plugin of plugins) config.plugins.set(plugin.name, plugin) 187 | config.pjson.oclif.plugins = ['@My/pluginb'] 188 | config.pjson.dependencies = {'@My/pluginb': '0.0.0'} 189 | for (const plugin of config.getPluginsList()) { 190 | // @ts-expect-error private method 191 | config.loadCommands(plugin) 192 | // @ts-expect-error private method 193 | config.loadTopics(plugin) 194 | } 195 | }) 196 | 197 | it('generates a valid completion file.', () => { 198 | config.bin = 'test-cli' 199 | const powerShellComp = new PowerShellComp(config as Config) 200 | expect(powerShellComp.generate()).to.equal(` 201 | using namespace System.Management.Automation 202 | using namespace System.Management.Automation.Language 203 | 204 | $scriptblock = { 205 | param($WordToComplete, $CommandAst, $CursorPosition) 206 | 207 | $Commands = 208 | @{ 209 | "app" = @{ 210 | "_summary" = "execute code" 211 | "execute" = @{ 212 | "_summary" = "execute code" 213 | "code" = @{ 214 | "_command" = @{ 215 | "summary" = "execute code" 216 | "flags" = @{ 217 | "help" = @{ "summary" = "Show help for command" } 218 | } 219 | } 220 | } 221 | 222 | } 223 | 224 | } 225 | 226 | "deploy" = @{ 227 | "_command" = @{ 228 | "summary" = "Deploy a project" 229 | "flags" = @{ 230 | "help" = @{ "summary" = "Show help for command" } 231 | "api-version" = @{ "summary" = " " } 232 | "ignore-errors" = @{ "summary" = "Ignore errors." } 233 | "json" = @{ "summary" = "Format output as ""json""." } 234 | "metadata" = @{ 235 | "summary" = " " 236 | "multiple" = $true 237 | } 238 | } 239 | } 240 | "functions" = @{ 241 | "_command" = @{ 242 | "summary" = "Deploy a function." 243 | "flags" = @{ 244 | "help" = @{ "summary" = "Show help for command" } 245 | "branch" = @{ "summary" = " " } 246 | } 247 | } 248 | } 249 | } 250 | 251 | "autocomplete" = @{ 252 | "_command" = @{ 253 | "summary" = "Display autocomplete installation instructions." 254 | "flags" = @{ 255 | "help" = @{ "summary" = "Show help for command" } 256 | "refresh-cache" = @{ "summary" = "Refresh cache (ignores displaying instructions)" } 257 | } 258 | } 259 | } 260 | 261 | "search" = @{ 262 | "_command" = @{ 263 | "summary" = "Search for a command" 264 | "flags" = @{ 265 | "help" = @{ "summary" = "Show help for command" } 266 | } 267 | } 268 | } 269 | 270 | } 271 | 272 | # Get the current mode 273 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 274 | 275 | # Everything in the current line except the CLI executable name. 276 | $CurrentLine = $commandAst.CommandElements[1..$commandAst.CommandElements.Count] -split " " 277 | 278 | # Remove $WordToComplete from the current line. 279 | if ($WordToComplete -ne "") { 280 | if ($CurrentLine.Count -eq 1) { 281 | $CurrentLine = @() 282 | } else { 283 | $CurrentLine = $CurrentLine[0..$CurrentLine.Count] 284 | } 285 | } 286 | 287 | # Save flags in current line without the \`--\` prefix. 288 | $Flags = $CurrentLine | Where-Object { 289 | $_ -Match "^-{1,2}(\\w+)" 290 | } | ForEach-Object { 291 | $_.trim("-") 292 | } 293 | # Set $flags to an empty hashtable if there are no flags in the current line. 294 | if ($Flags -eq $null) { 295 | $Flags = @{} 296 | } 297 | 298 | # No command in the current line, suggest top-level args. 299 | if ($CurrentLine.Count -eq 0) { 300 | $Commands.GetEnumerator() | Where-Object { 301 | $_.Key.StartsWith("$WordToComplete") 302 | } | Sort-Object -Property key | ForEach-Object { 303 | New-Object -Type CompletionResult -ArgumentList \` 304 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 305 | $_.Key, 306 | "ParameterValue", 307 | "$($_.Value._summary ?? $_.Value._command.summary ?? " ")" 308 | } 309 | } else { 310 | # Start completing command/topic/coTopic 311 | 312 | $NextArg = $null 313 | $PrevNode = $null 314 | 315 | # Iterate over the current line to find the command/topic/coTopic hashtable 316 | $CurrentLine | ForEach-Object { 317 | if ($NextArg -eq $null) { 318 | $NextArg = $Commands[$_] 319 | } elseif ($PrevNode[$_] -ne $null) { 320 | $NextArg = $PrevNode[$_] 321 | } elseif ($_.StartsWith('-')) { 322 | return 323 | } else { 324 | $NextArg = $PrevNode 325 | } 326 | 327 | $PrevNode = $NextArg 328 | } 329 | 330 | # Start completing command. 331 | if ($NextArg._command -ne $null) { 332 | # Complete flags 333 | # \`cli config list -\` 334 | if ($WordToComplete -like '-*') { 335 | $NextArg._command.flags.GetEnumerator() | Sort-Object -Property key 336 | | Where-Object { 337 | # Filter out already used flags (unless \`flag.multiple = true\`). 338 | $_.Key.StartsWith("$($WordToComplete.Trim("-"))") -and ($_.Value.multiple -eq $true -or !$flags.Contains($_.Key)) 339 | } 340 | | ForEach-Object { 341 | New-Object -Type CompletionResult -ArgumentList \` 342 | $($Mode -eq "MenuComplete" ? "--$($_.Key) " : "--$($_.Key)"), 343 | $_.Key, 344 | "ParameterValue", 345 | "$($NextArg._command.flags[$_.Key].summary ?? " ")" 346 | } 347 | } else { 348 | # This could be a coTopic. We remove the "_command" hashtable 349 | # from $NextArg and check if there's a command under the current partial ID. 350 | $NextArg.remove("_command") 351 | 352 | if ($NextArg.keys -gt 0) { 353 | $NextArg.GetEnumerator() | Where-Object { 354 | $_.Key.StartsWith("$WordToComplete") 355 | } | Sort-Object -Property key | ForEach-Object { 356 | New-Object -Type CompletionResult -ArgumentList \` 357 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 358 | $_.Key, 359 | "ParameterValue", 360 | "$($NextArg[$_.Key]._summary ?? " ")" 361 | } 362 | } 363 | } 364 | } else { 365 | # Start completing topic. 366 | 367 | # Topic summary is stored as "_summary" in the hashtable. 368 | # At this stage it is no longer needed so we remove it 369 | # so that $NextArg contains only commands/topics hashtables 370 | 371 | $NextArg.remove("_summary") 372 | 373 | $NextArg.GetEnumerator() | Where-Object { 374 | $_.Key.StartsWith("$WordToComplete") 375 | } | Sort-Object -Property key | ForEach-Object { 376 | New-Object -Type CompletionResult -ArgumentList \` 377 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 378 | $_.Key, 379 | "ParameterValue", 380 | "$($NextArg[$_.Key]._summary ?? $NextArg[$_.Key]._command.summary ?? " ")" 381 | } 382 | } 383 | } 384 | } 385 | Register-ArgumentCompleter -Native -CommandName test-cli -ScriptBlock $scriptblock 386 | `) 387 | }) 388 | 389 | it('generates a valid completion file with a bin alias.', () => { 390 | config.bin = 'test-cli' 391 | config.binAliases = ['test'] 392 | const powerShellComp = new PowerShellComp(config as Config) 393 | expect(powerShellComp.generate()).to.equal(` 394 | using namespace System.Management.Automation 395 | using namespace System.Management.Automation.Language 396 | 397 | $scriptblock = { 398 | param($WordToComplete, $CommandAst, $CursorPosition) 399 | 400 | $Commands = 401 | @{ 402 | "app" = @{ 403 | "_summary" = "execute code" 404 | "execute" = @{ 405 | "_summary" = "execute code" 406 | "code" = @{ 407 | "_command" = @{ 408 | "summary" = "execute code" 409 | "flags" = @{ 410 | "help" = @{ "summary" = "Show help for command" } 411 | } 412 | } 413 | } 414 | 415 | } 416 | 417 | } 418 | 419 | "deploy" = @{ 420 | "_command" = @{ 421 | "summary" = "Deploy a project" 422 | "flags" = @{ 423 | "help" = @{ "summary" = "Show help for command" } 424 | "api-version" = @{ "summary" = " " } 425 | "ignore-errors" = @{ "summary" = "Ignore errors." } 426 | "json" = @{ "summary" = "Format output as ""json""." } 427 | "metadata" = @{ 428 | "summary" = " " 429 | "multiple" = $true 430 | } 431 | } 432 | } 433 | "functions" = @{ 434 | "_command" = @{ 435 | "summary" = "Deploy a function." 436 | "flags" = @{ 437 | "help" = @{ "summary" = "Show help for command" } 438 | "branch" = @{ "summary" = " " } 439 | } 440 | } 441 | } 442 | } 443 | 444 | "autocomplete" = @{ 445 | "_command" = @{ 446 | "summary" = "Display autocomplete installation instructions." 447 | "flags" = @{ 448 | "help" = @{ "summary" = "Show help for command" } 449 | "refresh-cache" = @{ "summary" = "Refresh cache (ignores displaying instructions)" } 450 | } 451 | } 452 | } 453 | 454 | "search" = @{ 455 | "_command" = @{ 456 | "summary" = "Search for a command" 457 | "flags" = @{ 458 | "help" = @{ "summary" = "Show help for command" } 459 | } 460 | } 461 | } 462 | 463 | } 464 | 465 | # Get the current mode 466 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 467 | 468 | # Everything in the current line except the CLI executable name. 469 | $CurrentLine = $commandAst.CommandElements[1..$commandAst.CommandElements.Count] -split " " 470 | 471 | # Remove $WordToComplete from the current line. 472 | if ($WordToComplete -ne "") { 473 | if ($CurrentLine.Count -eq 1) { 474 | $CurrentLine = @() 475 | } else { 476 | $CurrentLine = $CurrentLine[0..$CurrentLine.Count] 477 | } 478 | } 479 | 480 | # Save flags in current line without the \`--\` prefix. 481 | $Flags = $CurrentLine | Where-Object { 482 | $_ -Match "^-{1,2}(\\w+)" 483 | } | ForEach-Object { 484 | $_.trim("-") 485 | } 486 | # Set $flags to an empty hashtable if there are no flags in the current line. 487 | if ($Flags -eq $null) { 488 | $Flags = @{} 489 | } 490 | 491 | # No command in the current line, suggest top-level args. 492 | if ($CurrentLine.Count -eq 0) { 493 | $Commands.GetEnumerator() | Where-Object { 494 | $_.Key.StartsWith("$WordToComplete") 495 | } | Sort-Object -Property key | ForEach-Object { 496 | New-Object -Type CompletionResult -ArgumentList \` 497 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 498 | $_.Key, 499 | "ParameterValue", 500 | "$($_.Value._summary ?? $_.Value._command.summary ?? " ")" 501 | } 502 | } else { 503 | # Start completing command/topic/coTopic 504 | 505 | $NextArg = $null 506 | $PrevNode = $null 507 | 508 | # Iterate over the current line to find the command/topic/coTopic hashtable 509 | $CurrentLine | ForEach-Object { 510 | if ($NextArg -eq $null) { 511 | $NextArg = $Commands[$_] 512 | } elseif ($PrevNode[$_] -ne $null) { 513 | $NextArg = $PrevNode[$_] 514 | } elseif ($_.StartsWith('-')) { 515 | return 516 | } else { 517 | $NextArg = $PrevNode 518 | } 519 | 520 | $PrevNode = $NextArg 521 | } 522 | 523 | # Start completing command. 524 | if ($NextArg._command -ne $null) { 525 | # Complete flags 526 | # \`cli config list -\` 527 | if ($WordToComplete -like '-*') { 528 | $NextArg._command.flags.GetEnumerator() | Sort-Object -Property key 529 | | Where-Object { 530 | # Filter out already used flags (unless \`flag.multiple = true\`). 531 | $_.Key.StartsWith("$($WordToComplete.Trim("-"))") -and ($_.Value.multiple -eq $true -or !$flags.Contains($_.Key)) 532 | } 533 | | ForEach-Object { 534 | New-Object -Type CompletionResult -ArgumentList \` 535 | $($Mode -eq "MenuComplete" ? "--$($_.Key) " : "--$($_.Key)"), 536 | $_.Key, 537 | "ParameterValue", 538 | "$($NextArg._command.flags[$_.Key].summary ?? " ")" 539 | } 540 | } else { 541 | # This could be a coTopic. We remove the "_command" hashtable 542 | # from $NextArg and check if there's a command under the current partial ID. 543 | $NextArg.remove("_command") 544 | 545 | if ($NextArg.keys -gt 0) { 546 | $NextArg.GetEnumerator() | Where-Object { 547 | $_.Key.StartsWith("$WordToComplete") 548 | } | Sort-Object -Property key | ForEach-Object { 549 | New-Object -Type CompletionResult -ArgumentList \` 550 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 551 | $_.Key, 552 | "ParameterValue", 553 | "$($NextArg[$_.Key]._summary ?? " ")" 554 | } 555 | } 556 | } 557 | } else { 558 | # Start completing topic. 559 | 560 | # Topic summary is stored as "_summary" in the hashtable. 561 | # At this stage it is no longer needed so we remove it 562 | # so that $NextArg contains only commands/topics hashtables 563 | 564 | $NextArg.remove("_summary") 565 | 566 | $NextArg.GetEnumerator() | Where-Object { 567 | $_.Key.StartsWith("$WordToComplete") 568 | } | Sort-Object -Property key | ForEach-Object { 569 | New-Object -Type CompletionResult -ArgumentList \` 570 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 571 | $_.Key, 572 | "ParameterValue", 573 | "$($NextArg[$_.Key]._summary ?? $NextArg[$_.Key]._command.summary ?? " ")" 574 | } 575 | } 576 | } 577 | } 578 | Register-ArgumentCompleter -Native -CommandName @("test","test-cli") -ScriptBlock $scriptblock 579 | `) 580 | }) 581 | 582 | it('generates a valid completion file with multiple bin aliases.', () => { 583 | config.bin = 'test-cli' 584 | config.binAliases = ['test', 'test1'] 585 | const powerShellComp = new PowerShellComp(config as Config) 586 | expect(powerShellComp.generate()).to.equal(` 587 | using namespace System.Management.Automation 588 | using namespace System.Management.Automation.Language 589 | 590 | $scriptblock = { 591 | param($WordToComplete, $CommandAst, $CursorPosition) 592 | 593 | $Commands = 594 | @{ 595 | "app" = @{ 596 | "_summary" = "execute code" 597 | "execute" = @{ 598 | "_summary" = "execute code" 599 | "code" = @{ 600 | "_command" = @{ 601 | "summary" = "execute code" 602 | "flags" = @{ 603 | "help" = @{ "summary" = "Show help for command" } 604 | } 605 | } 606 | } 607 | 608 | } 609 | 610 | } 611 | 612 | "deploy" = @{ 613 | "_command" = @{ 614 | "summary" = "Deploy a project" 615 | "flags" = @{ 616 | "help" = @{ "summary" = "Show help for command" } 617 | "api-version" = @{ "summary" = " " } 618 | "ignore-errors" = @{ "summary" = "Ignore errors." } 619 | "json" = @{ "summary" = "Format output as ""json""." } 620 | "metadata" = @{ 621 | "summary" = " " 622 | "multiple" = $true 623 | } 624 | } 625 | } 626 | "functions" = @{ 627 | "_command" = @{ 628 | "summary" = "Deploy a function." 629 | "flags" = @{ 630 | "help" = @{ "summary" = "Show help for command" } 631 | "branch" = @{ "summary" = " " } 632 | } 633 | } 634 | } 635 | } 636 | 637 | "autocomplete" = @{ 638 | "_command" = @{ 639 | "summary" = "Display autocomplete installation instructions." 640 | "flags" = @{ 641 | "help" = @{ "summary" = "Show help for command" } 642 | "refresh-cache" = @{ "summary" = "Refresh cache (ignores displaying instructions)" } 643 | } 644 | } 645 | } 646 | 647 | "search" = @{ 648 | "_command" = @{ 649 | "summary" = "Search for a command" 650 | "flags" = @{ 651 | "help" = @{ "summary" = "Show help for command" } 652 | } 653 | } 654 | } 655 | 656 | } 657 | 658 | # Get the current mode 659 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 660 | 661 | # Everything in the current line except the CLI executable name. 662 | $CurrentLine = $commandAst.CommandElements[1..$commandAst.CommandElements.Count] -split " " 663 | 664 | # Remove $WordToComplete from the current line. 665 | if ($WordToComplete -ne "") { 666 | if ($CurrentLine.Count -eq 1) { 667 | $CurrentLine = @() 668 | } else { 669 | $CurrentLine = $CurrentLine[0..$CurrentLine.Count] 670 | } 671 | } 672 | 673 | # Save flags in current line without the \`--\` prefix. 674 | $Flags = $CurrentLine | Where-Object { 675 | $_ -Match "^-{1,2}(\\w+)" 676 | } | ForEach-Object { 677 | $_.trim("-") 678 | } 679 | # Set $flags to an empty hashtable if there are no flags in the current line. 680 | if ($Flags -eq $null) { 681 | $Flags = @{} 682 | } 683 | 684 | # No command in the current line, suggest top-level args. 685 | if ($CurrentLine.Count -eq 0) { 686 | $Commands.GetEnumerator() | Where-Object { 687 | $_.Key.StartsWith("$WordToComplete") 688 | } | Sort-Object -Property key | ForEach-Object { 689 | New-Object -Type CompletionResult -ArgumentList \` 690 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 691 | $_.Key, 692 | "ParameterValue", 693 | "$($_.Value._summary ?? $_.Value._command.summary ?? " ")" 694 | } 695 | } else { 696 | # Start completing command/topic/coTopic 697 | 698 | $NextArg = $null 699 | $PrevNode = $null 700 | 701 | # Iterate over the current line to find the command/topic/coTopic hashtable 702 | $CurrentLine | ForEach-Object { 703 | if ($NextArg -eq $null) { 704 | $NextArg = $Commands[$_] 705 | } elseif ($PrevNode[$_] -ne $null) { 706 | $NextArg = $PrevNode[$_] 707 | } elseif ($_.StartsWith('-')) { 708 | return 709 | } else { 710 | $NextArg = $PrevNode 711 | } 712 | 713 | $PrevNode = $NextArg 714 | } 715 | 716 | # Start completing command. 717 | if ($NextArg._command -ne $null) { 718 | # Complete flags 719 | # \`cli config list -\` 720 | if ($WordToComplete -like '-*') { 721 | $NextArg._command.flags.GetEnumerator() | Sort-Object -Property key 722 | | Where-Object { 723 | # Filter out already used flags (unless \`flag.multiple = true\`). 724 | $_.Key.StartsWith("$($WordToComplete.Trim("-"))") -and ($_.Value.multiple -eq $true -or !$flags.Contains($_.Key)) 725 | } 726 | | ForEach-Object { 727 | New-Object -Type CompletionResult -ArgumentList \` 728 | $($Mode -eq "MenuComplete" ? "--$($_.Key) " : "--$($_.Key)"), 729 | $_.Key, 730 | "ParameterValue", 731 | "$($NextArg._command.flags[$_.Key].summary ?? " ")" 732 | } 733 | } else { 734 | # This could be a coTopic. We remove the "_command" hashtable 735 | # from $NextArg and check if there's a command under the current partial ID. 736 | $NextArg.remove("_command") 737 | 738 | if ($NextArg.keys -gt 0) { 739 | $NextArg.GetEnumerator() | Where-Object { 740 | $_.Key.StartsWith("$WordToComplete") 741 | } | Sort-Object -Property key | ForEach-Object { 742 | New-Object -Type CompletionResult -ArgumentList \` 743 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 744 | $_.Key, 745 | "ParameterValue", 746 | "$($NextArg[$_.Key]._summary ?? " ")" 747 | } 748 | } 749 | } 750 | } else { 751 | # Start completing topic. 752 | 753 | # Topic summary is stored as "_summary" in the hashtable. 754 | # At this stage it is no longer needed so we remove it 755 | # so that $NextArg contains only commands/topics hashtables 756 | 757 | $NextArg.remove("_summary") 758 | 759 | $NextArg.GetEnumerator() | Where-Object { 760 | $_.Key.StartsWith("$WordToComplete") 761 | } | Sort-Object -Property key | ForEach-Object { 762 | New-Object -Type CompletionResult -ArgumentList \` 763 | $($Mode -eq "MenuComplete" ? "$($_.Key) " : "$($_.Key)"), 764 | $_.Key, 765 | "ParameterValue", 766 | "$($NextArg[$_.Key]._summary ?? $NextArg[$_.Key]._command.summary ?? " ")" 767 | } 768 | } 769 | } 770 | } 771 | Register-ArgumentCompleter -Native -CommandName @("test","test1","test-cli") -ScriptBlock $scriptblock 772 | `) 773 | }) 774 | }) 775 | -------------------------------------------------------------------------------- /test/autocomplete/zsh.test.ts: -------------------------------------------------------------------------------- 1 | import {Command, Config} from '@oclif/core' 2 | import {Deprecation, Plugin as IPlugin} from '@oclif/core/interfaces' 3 | import {expect} from 'chai' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | import ZshCompWithSpaces from '../../src/autocomplete/zsh.js' 8 | // autocomplete will throw error on windows ci 9 | import {default as skipWindows} from '../helpers/runtest.js' 10 | 11 | class MyCommandClass implements Command.Cached { 12 | [key: string]: unknown 13 | _base = '' 14 | aliases: string[] = [] 15 | aliasPermutations?: string[] | undefined 16 | args: {[name: string]: Command.Arg.Cached} = {} 17 | deprecateAliases?: boolean | undefined 18 | deprecationOptions?: Deprecation | undefined 19 | description?: string | undefined 20 | examples?: Command.Example[] | undefined 21 | flags = {} 22 | hasDynamicHelp?: boolean | undefined 23 | hidden = false 24 | hiddenAliases!: string[] 25 | id = 'foo:bar' 26 | isESM?: boolean | undefined 27 | permutations?: string[] | undefined 28 | pluginAlias?: string | undefined 29 | pluginName?: string | undefined 30 | pluginType?: string | undefined 31 | relativePath?: string[] | undefined 32 | state?: string | undefined 33 | strict?: boolean | undefined 34 | summary?: string | undefined 35 | type?: string | undefined 36 | usage?: string | string[] | undefined 37 | 38 | new(): Command.Cached { 39 | // @ts-expect-error this is not the full interface but enough for testing 40 | return { 41 | _run(): Promise { 42 | return Promise.resolve() 43 | }, 44 | } 45 | } 46 | 47 | run(): PromiseLike { 48 | return Promise.resolve() 49 | } 50 | } 51 | 52 | const commandPluginA: Command.Loadable = { 53 | aliases: [], 54 | args: {}, 55 | flags: { 56 | 'api-version': { 57 | char: 'a', 58 | multiple: false, 59 | name: 'api-version', 60 | type: 'option', 61 | }, 62 | 'ignore-errors': { 63 | allowNo: false, 64 | char: 'i', 65 | name: 'ignore-errors', 66 | summary: 'Ignore errors.', 67 | type: 'boolean', 68 | }, 69 | json: { 70 | allowNo: false, 71 | name: 'json', 72 | summary: 'Format output as json.', 73 | type: 'boolean', 74 | }, 75 | metadata: { 76 | char: 'm', 77 | multiple: true, 78 | name: 'metadata', 79 | type: 'option', 80 | }, 81 | }, 82 | hidden: false, 83 | hiddenAliases: [], 84 | id: 'deploy', 85 | async load(): Promise { 86 | return new MyCommandClass() as unknown as Command.Class 87 | }, 88 | pluginAlias: '@My/plugina', 89 | pluginType: 'core', 90 | strict: false, 91 | summary: 'Deploy a project', 92 | } 93 | 94 | const commandPluginB: Command.Loadable = { 95 | aliases: [], 96 | args: {}, 97 | flags: { 98 | branch: { 99 | char: 'b', 100 | multiple: false, 101 | name: 'branch', 102 | type: 'option', 103 | }, 104 | }, 105 | hidden: false, 106 | hiddenAliases: [], 107 | id: 'deploy:functions', 108 | async load(): Promise { 109 | return new MyCommandClass() as unknown as Command.Class 110 | }, 111 | pluginAlias: '@My/pluginb', 112 | pluginType: 'core', 113 | strict: false, 114 | summary: 'Deploy a function.', 115 | } 116 | 117 | const commandPluginC: Command.Loadable = { 118 | aliases: [], 119 | args: {}, 120 | flags: {}, 121 | hidden: false, 122 | hiddenAliases: [], 123 | id: 'search', 124 | async load(): Promise { 125 | return new MyCommandClass() as unknown as Command.Class 126 | }, 127 | pluginAlias: '@My/pluginc', 128 | pluginType: 'core', 129 | strict: false, 130 | summary: 'Search for a command', 131 | } 132 | 133 | const commandPluginD: Command.Loadable = { 134 | aliases: [], 135 | args: {}, 136 | flags: {}, 137 | hidden: false, 138 | hiddenAliases: [], 139 | id: 'app:execute:code', 140 | async load(): Promise { 141 | return new MyCommandClass() as unknown as Command.Class 142 | }, 143 | pluginAlias: '@My/plugind', 144 | pluginType: 'core', 145 | strict: false, 146 | summary: 'execute code', 147 | } 148 | 149 | const pluginA: IPlugin = { 150 | _base: '', 151 | alias: '@My/plugina', 152 | commandIDs: ['deploy'], 153 | commands: [commandPluginA, commandPluginB, commandPluginC, commandPluginD], 154 | commandsDir: '', 155 | async findCommand(): Promise { 156 | return new MyCommandClass() as unknown as Command.Class 157 | }, 158 | hasManifest: false, 159 | hooks: {}, 160 | isRoot: false, 161 | async load(): Promise {}, 162 | moduleType: 'commonjs', 163 | name: '@My/plugina', 164 | options: { 165 | root: '', 166 | }, 167 | pjson: {} as any, 168 | root: '', 169 | tag: 'tag', 170 | topics: [ 171 | { 172 | description: 'foo commands', 173 | name: 'foo', 174 | }, 175 | ], 176 | type: 'core', 177 | valid: true, 178 | version: '0.0.0', 179 | } 180 | 181 | const plugins: IPlugin[] = [pluginA] 182 | 183 | skipWindows('zsh comp', () => { 184 | describe('zsh completion with spaces', () => { 185 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json') 186 | const config = new Config({root}) 187 | 188 | before(async () => { 189 | await config.load() 190 | 191 | for (const plugin of plugins) config.plugins.set(plugin.name, plugin) 192 | config.plugins.delete('@oclif/plugin-autocomplete') 193 | config.pjson.oclif.plugins = ['@My/pluginb'] 194 | config.pjson.dependencies = {'@My/pluginb': '0.0.0'} 195 | for (const plugin of config.getPluginsList()) { 196 | // @ts-expect-error private method 197 | config.loadCommands(plugin) 198 | // @ts-expect-error private method 199 | config.loadTopics(plugin) 200 | } 201 | }) 202 | 203 | it('generates a valid completion file.', () => { 204 | config.bin = 'test-cli' 205 | const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) 206 | expect(zshCompWithSpaces.generate()).to.equal(`#compdef test-cli 207 | 208 | 209 | _test-cli_app() { 210 | local context state state_descr line 211 | typeset -A opt_args 212 | 213 | _arguments -C "1: :->cmds" "*::arg:->args" 214 | 215 | case "$state" in 216 | cmds) 217 | _values "completions" \\ 218 | "execute[execute code]" \\ 219 | 220 | ;; 221 | args) 222 | case $line[1] in 223 | "execute") 224 | _test-cli_app_execute 225 | ;; 226 | 227 | esac 228 | ;; 229 | esac 230 | } 231 | 232 | _test-cli_app_execute() { 233 | local context state state_descr line 234 | typeset -A opt_args 235 | 236 | _arguments -C "1: :->cmds" "*::arg:->args" 237 | 238 | case "$state" in 239 | cmds) 240 | _values "completions" \\ 241 | "code[execute code]" \\ 242 | 243 | ;; 244 | args) 245 | case $line[1] in 246 | "code") 247 | _arguments -S \\ 248 | --help"[Show help for command]" \\ 249 | "*: :_files" 250 | ;; 251 | 252 | esac 253 | ;; 254 | esac 255 | } 256 | 257 | _test-cli_deploy() { 258 | _test-cli_deploy_flags() { 259 | local context state state_descr line 260 | typeset -A opt_args 261 | 262 | _arguments -S \\ 263 | "(-a --api-version)"{-a,--api-version}"[]:file:_files" \\ 264 | "(-i --ignore-errors)"{-i,--ignore-errors}"[Ignore errors.]" \\ 265 | --json"[Format output as json.]" \\ 266 | "*"{-m,--metadata}"[]:file:_files" \\ 267 | --help"[Show help for command]" \\ 268 | "*: :_files" 269 | } 270 | 271 | local context state state_descr line 272 | typeset -A opt_args 273 | 274 | _arguments -C "1: :->cmds" "*: :->args" 275 | 276 | case "$state" in 277 | cmds) 278 | if [[ "\${words[CURRENT]}" == -* ]]; then 279 | _test-cli_deploy_flags 280 | else 281 | _values "completions" \\ 282 | "functions[Deploy a function.]" \\ 283 | 284 | fi 285 | ;; 286 | args) 287 | case $line[1] in 288 | "functions") 289 | _arguments -S \\ 290 | "(-b --branch)"{-b,--branch}"[]:file:_files" \\ 291 | --help"[Show help for command]" \\ 292 | "*: :_files" 293 | ;; 294 | 295 | *) 296 | _test-cli_deploy_flags 297 | ;; 298 | esac 299 | ;; 300 | esac 301 | } 302 | 303 | 304 | _test-cli() { 305 | local context state state_descr line 306 | typeset -A opt_args 307 | 308 | _arguments -C "1: :->cmds" "*::arg:->args" 309 | 310 | case "$state" in 311 | cmds) 312 | _values "completions" \\ 313 | "app[execute code]" \\ 314 | "deploy[Deploy a project]" \\ 315 | "search[Search for a command]" \\ 316 | 317 | ;; 318 | args) 319 | case $line[1] in 320 | app) 321 | _test-cli_app 322 | ;; 323 | deploy) 324 | _test-cli_deploy 325 | ;; 326 | esac 327 | 328 | ;; 329 | esac 330 | } 331 | 332 | _test-cli 333 | `) 334 | }) 335 | 336 | it('generates a valid completion file with a bin alias.', () => { 337 | config.bin = 'test-cli' 338 | config.binAliases = ['testing'] 339 | const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) 340 | expect(zshCompWithSpaces.generate()).to.equal(`#compdef test-cli 341 | compdef testing=test-cli 342 | 343 | _test-cli_app() { 344 | local context state state_descr line 345 | typeset -A opt_args 346 | 347 | _arguments -C "1: :->cmds" "*::arg:->args" 348 | 349 | case "$state" in 350 | cmds) 351 | _values "completions" \\ 352 | "execute[execute code]" \\ 353 | 354 | ;; 355 | args) 356 | case $line[1] in 357 | "execute") 358 | _test-cli_app_execute 359 | ;; 360 | 361 | esac 362 | ;; 363 | esac 364 | } 365 | 366 | _test-cli_app_execute() { 367 | local context state state_descr line 368 | typeset -A opt_args 369 | 370 | _arguments -C "1: :->cmds" "*::arg:->args" 371 | 372 | case "$state" in 373 | cmds) 374 | _values "completions" \\ 375 | "code[execute code]" \\ 376 | 377 | ;; 378 | args) 379 | case $line[1] in 380 | "code") 381 | _arguments -S \\ 382 | --help"[Show help for command]" \\ 383 | "*: :_files" 384 | ;; 385 | 386 | esac 387 | ;; 388 | esac 389 | } 390 | 391 | _test-cli_deploy() { 392 | _test-cli_deploy_flags() { 393 | local context state state_descr line 394 | typeset -A opt_args 395 | 396 | _arguments -S \\ 397 | "(-a --api-version)"{-a,--api-version}"[]:file:_files" \\ 398 | "(-i --ignore-errors)"{-i,--ignore-errors}"[Ignore errors.]" \\ 399 | --json"[Format output as json.]" \\ 400 | "*"{-m,--metadata}"[]:file:_files" \\ 401 | --help"[Show help for command]" \\ 402 | "*: :_files" 403 | } 404 | 405 | local context state state_descr line 406 | typeset -A opt_args 407 | 408 | _arguments -C "1: :->cmds" "*: :->args" 409 | 410 | case "$state" in 411 | cmds) 412 | if [[ "\${words[CURRENT]}" == -* ]]; then 413 | _test-cli_deploy_flags 414 | else 415 | _values "completions" \\ 416 | "functions[Deploy a function.]" \\ 417 | 418 | fi 419 | ;; 420 | args) 421 | case $line[1] in 422 | "functions") 423 | _arguments -S \\ 424 | "(-b --branch)"{-b,--branch}"[]:file:_files" \\ 425 | --help"[Show help for command]" \\ 426 | "*: :_files" 427 | ;; 428 | 429 | *) 430 | _test-cli_deploy_flags 431 | ;; 432 | esac 433 | ;; 434 | esac 435 | } 436 | 437 | 438 | _test-cli() { 439 | local context state state_descr line 440 | typeset -A opt_args 441 | 442 | _arguments -C "1: :->cmds" "*::arg:->args" 443 | 444 | case "$state" in 445 | cmds) 446 | _values "completions" \\ 447 | "app[execute code]" \\ 448 | "deploy[Deploy a project]" \\ 449 | "search[Search for a command]" \\ 450 | 451 | ;; 452 | args) 453 | case $line[1] in 454 | app) 455 | _test-cli_app 456 | ;; 457 | deploy) 458 | _test-cli_deploy 459 | ;; 460 | esac 461 | 462 | ;; 463 | esac 464 | } 465 | 466 | _test-cli 467 | `) 468 | }) 469 | 470 | it('generates a valid completion file with multiple bin aliases.', () => { 471 | config.bin = 'test-cli' 472 | config.binAliases = ['testing', 'testing2'] 473 | const zshCompWithSpaces = new ZshCompWithSpaces(config as Config) 474 | expect(zshCompWithSpaces.generate()).to.equal(`#compdef test-cli 475 | compdef testing=test-cli 476 | compdef testing2=test-cli 477 | 478 | _test-cli_app() { 479 | local context state state_descr line 480 | typeset -A opt_args 481 | 482 | _arguments -C "1: :->cmds" "*::arg:->args" 483 | 484 | case "$state" in 485 | cmds) 486 | _values "completions" \\ 487 | "execute[execute code]" \\ 488 | 489 | ;; 490 | args) 491 | case $line[1] in 492 | "execute") 493 | _test-cli_app_execute 494 | ;; 495 | 496 | esac 497 | ;; 498 | esac 499 | } 500 | 501 | _test-cli_app_execute() { 502 | local context state state_descr line 503 | typeset -A opt_args 504 | 505 | _arguments -C "1: :->cmds" "*::arg:->args" 506 | 507 | case "$state" in 508 | cmds) 509 | _values "completions" \\ 510 | "code[execute code]" \\ 511 | 512 | ;; 513 | args) 514 | case $line[1] in 515 | "code") 516 | _arguments -S \\ 517 | --help"[Show help for command]" \\ 518 | "*: :_files" 519 | ;; 520 | 521 | esac 522 | ;; 523 | esac 524 | } 525 | 526 | _test-cli_deploy() { 527 | _test-cli_deploy_flags() { 528 | local context state state_descr line 529 | typeset -A opt_args 530 | 531 | _arguments -S \\ 532 | "(-a --api-version)"{-a,--api-version}"[]:file:_files" \\ 533 | "(-i --ignore-errors)"{-i,--ignore-errors}"[Ignore errors.]" \\ 534 | --json"[Format output as json.]" \\ 535 | "*"{-m,--metadata}"[]:file:_files" \\ 536 | --help"[Show help for command]" \\ 537 | "*: :_files" 538 | } 539 | 540 | local context state state_descr line 541 | typeset -A opt_args 542 | 543 | _arguments -C "1: :->cmds" "*: :->args" 544 | 545 | case "$state" in 546 | cmds) 547 | if [[ "\${words[CURRENT]}" == -* ]]; then 548 | _test-cli_deploy_flags 549 | else 550 | _values "completions" \\ 551 | "functions[Deploy a function.]" \\ 552 | 553 | fi 554 | ;; 555 | args) 556 | case $line[1] in 557 | "functions") 558 | _arguments -S \\ 559 | "(-b --branch)"{-b,--branch}"[]:file:_files" \\ 560 | --help"[Show help for command]" \\ 561 | "*: :_files" 562 | ;; 563 | 564 | *) 565 | _test-cli_deploy_flags 566 | ;; 567 | esac 568 | ;; 569 | esac 570 | } 571 | 572 | 573 | _test-cli() { 574 | local context state state_descr line 575 | typeset -A opt_args 576 | 577 | _arguments -C "1: :->cmds" "*::arg:->args" 578 | 579 | case "$state" in 580 | cmds) 581 | _values "completions" \\ 582 | "app[execute code]" \\ 583 | "deploy[Deploy a project]" \\ 584 | "search[Search for a command]" \\ 585 | 586 | ;; 587 | args) 588 | case $line[1] in 589 | app) 590 | _test-cli_app 591 | ;; 592 | deploy) 593 | _test-cli_deploy 594 | ;; 595 | esac 596 | 597 | ;; 598 | esac 599 | } 600 | 601 | _test-cli 602 | `) 603 | }) 604 | }) 605 | }) 606 | -------------------------------------------------------------------------------- /test/base.test.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '@oclif/core' 2 | import {expect} from 'chai' 3 | import {readFile, rm} from 'node:fs/promises' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | import {AutocompleteBase} from '../src/base.js' 8 | 9 | class AutocompleteTest extends AutocompleteBase { 10 | async run() { 11 | return null 12 | } 13 | } 14 | 15 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json') 16 | const config = new Config({root}) 17 | 18 | const cmd = new AutocompleteTest([], config) 19 | 20 | describe('AutocompleteBase', () => { 21 | beforeEach(async () => { 22 | await config.load() 23 | }) 24 | 25 | it('#convertWindowsBash', () => { 26 | expect(cmd.determineShell('bash')).to.eq('bash') 27 | expect(cmd.determineShell('zsh')).to.eq('zsh') 28 | expect(cmd.determineShell('fish')).to.eq('fish') 29 | expect(cmd.determineShell('C:\\Users\\someone\\bin\\bash.exe')).to.eq('bash') 30 | expect(() => { 31 | cmd.determineShell('') 32 | }).to.throw() 33 | }) 34 | 35 | it('#autocompleteCacheDir', () => { 36 | expect(cmd.autocompleteCacheDir).to.eq(path.join(config.cacheDir, 'autocomplete')) 37 | }) 38 | 39 | it('#acLogfile', async () => { 40 | expect(cmd.acLogfilePath).to.eq(path.join(config.cacheDir, 'autocomplete.log')) 41 | await rm(cmd.acLogfilePath, {force: true, recursive: true}) 42 | cmd.writeLogFile('testing') 43 | 44 | const logs = await readFile(cmd.acLogfilePath, 'utf8') 45 | expect(logs).to.include('testing') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/commands/autocomplete/create.test.ts: -------------------------------------------------------------------------------- 1 | import {Config, Interfaces, Plugin} from '@oclif/core' 2 | import {expect} from 'chai' 3 | import {readFile} from 'node:fs/promises' 4 | import path from 'node:path' 5 | import {fileURLToPath} from 'node:url' 6 | 7 | import Create from '../../../src/commands/autocomplete/create.js' 8 | // autocomplete will throw error on windows ci 9 | import {default as skipWindows} from '../../helpers/runtest.js' 10 | 11 | const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../package.json') 12 | const config = new Config({root}) 13 | 14 | const readJson = async (path: string): Promise => { 15 | const contents = await readFile(path, 'utf8') 16 | return JSON.parse(contents) 17 | } 18 | 19 | skipWindows('Create', () => { 20 | // Unit test private methods for extra coverage 21 | describe('private methods', () => { 22 | let cmd: any 23 | let plugin: any 24 | 25 | before(async () => { 26 | await config.load() 27 | cmd = new Create([], config) 28 | plugin = new Plugin({root}) 29 | cmd.config.plugins = [plugin] 30 | plugin._manifest = () => 31 | readJson(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../test.oclif.manifest.json')) 32 | await plugin.load() 33 | }) 34 | 35 | it('file paths', () => { 36 | const dir = cmd.config.cacheDir 37 | expect(cmd.bashSetupScriptPath).to.eq(`${dir}/autocomplete/bash_setup`) 38 | expect(cmd.bashCompletionFunctionPath).to.eq(`${dir}/autocomplete/functions/bash/oclif-example.bash`) 39 | expect(cmd.zshSetupScriptPath).to.eq(`${dir}/autocomplete/zsh_setup`) 40 | expect(cmd.zshCompletionFunctionPath).to.eq(`${dir}/autocomplete/functions/zsh/_oclif-example`) 41 | }) 42 | 43 | it('#bashSetupScript', () => { 44 | expect(cmd.bashSetupScript).to.eq( 45 | `OCLIF_EXAMPLE_AC_BASH_COMPFUNC_PATH=${config.cacheDir}/autocomplete/functions/bash/oclif-example.bash && test -f $OCLIF_EXAMPLE_AC_BASH_COMPFUNC_PATH && source $OCLIF_EXAMPLE_AC_BASH_COMPFUNC_PATH;\n`, 46 | ) 47 | }) 48 | 49 | it('#bashSetupScript wiht more than one dash', async () => { 50 | const confitWithDash = new Config({root}) 51 | await confitWithDash.load() 52 | confitWithDash.bin = 'oclif-cli-example' 53 | const cmdWithDash: any = new Create([], confitWithDash) 54 | expect(cmdWithDash.bashSetupScript).to.eq( 55 | `OCLIF_CLI_EXAMPLE_AC_BASH_COMPFUNC_PATH=${config.cacheDir}/autocomplete/functions/bash/oclif-cli-example.bash && test -f $OCLIF_CLI_EXAMPLE_AC_BASH_COMPFUNC_PATH && source $OCLIF_CLI_EXAMPLE_AC_BASH_COMPFUNC_PATH;\n`, 56 | ) 57 | }) 58 | 59 | it('#zshSetupScript', () => { 60 | expect(cmd.zshSetupScript).to.eq(` 61 | fpath=( 62 | ${config.cacheDir}/autocomplete/functions/zsh 63 | $fpath 64 | ); 65 | autoload -Uz compinit; 66 | compinit; 67 | `) 68 | }) 69 | 70 | it('#bashCompletionFunction', () => { 71 | expect(cmd.bashCompletionFunction).to.eq(`#!/usr/bin/env bash 72 | 73 | _oclif-example_autocomplete() 74 | { 75 | 76 | local cur="\${COMP_WORDS[COMP_CWORD]}" opts IFS=$' \\t\\n' 77 | COMPREPLY=() 78 | 79 | local commands=" 80 | autocomplete --skip-instructions 81 | autocomplete:foo --bar --baz --dangerous --brackets --double-quotes --multi-line --json 82 | foo --bar --baz --dangerous --brackets --double-quotes --multi-line --json 83 | " 84 | 85 | if [[ "$cur" != "-"* ]]; then 86 | opts=$(printf "$commands" | grep -Eo '^[a-zA-Z0-9:_-]+') 87 | else 88 | local __COMP_WORDS 89 | if [[ \${COMP_WORDS[2]} == ":" ]]; then 90 | #subcommand 91 | __COMP_WORDS=$(printf "%s" "\${COMP_WORDS[@]:1:3}") 92 | else 93 | #simple command 94 | __COMP_WORDS="\${COMP_WORDS[@]:1:1}" 95 | fi 96 | opts=$(printf "$commands" | grep "\${__COMP_WORDS}" | sed -n "s/^\${__COMP_WORDS} //p") 97 | fi 98 | _get_comp_words_by_ref -n : cur 99 | COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) ) 100 | __ltrim_colon_completions "$cur" 101 | return 0 102 | 103 | } 104 | 105 | complete -o default -F _oclif-example_autocomplete oclif-example\n`) 106 | }) 107 | 108 | it('#bashCompletionFunction with spaces', async () => { 109 | const spacedConfig = new Config({root}) 110 | 111 | await spacedConfig.load() 112 | spacedConfig.topicSeparator = ' ' 113 | // : any is required for the next two lines otherwise ts will complain about _manifest and bashCompletionFunction being private down below 114 | const spacedCmd: any = new Create([], spacedConfig) 115 | const spacedPlugin: any = new Plugin({root}) 116 | spacedCmd.config.plugins = [spacedPlugin] 117 | spacedPlugin._manifest = () => 118 | readJson(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../test.oclif.manifest.json')) 119 | await spacedPlugin.load() 120 | 121 | expect(spacedCmd.bashCompletionFunction).to.eq(`#!/usr/bin/env bash 122 | 123 | # This function joins an array using a character passed in 124 | # e.g. ARRAY=(one two three) -> join_by ":" \${ARRAY[@]} -> "one:two:three" 125 | function join_by { local IFS="$1"; shift; echo "$*"; } 126 | 127 | _oclif-example_autocomplete() 128 | { 129 | 130 | local cur="\${COMP_WORDS[COMP_CWORD]}" opts normalizedCommand colonPrefix IFS=$' \\t\\n' 131 | COMPREPLY=() 132 | 133 | local commands=" 134 | autocomplete --skip-instructions 135 | autocomplete:foo --bar --baz --dangerous --brackets --double-quotes --multi-line --json 136 | foo --bar --baz --dangerous --brackets --double-quotes --multi-line --json 137 | " 138 | 139 | function __trim_colon_commands() 140 | { 141 | # Turn $commands into an array 142 | commands=("\${commands[@]}") 143 | 144 | if [[ -z "$colonPrefix" ]]; then 145 | colonPrefix="$normalizedCommand:" 146 | fi 147 | 148 | # Remove colon-word prefix from $commands 149 | commands=( "\${commands[@]/$colonPrefix}" ) 150 | 151 | for i in "\${!commands[@]}"; do 152 | if [[ "\${commands[$i]}" == "$normalizedCommand" ]]; then 153 | # If the currently typed in command is a topic command we need to remove it to avoid suggesting it again 154 | unset "\${commands[$i]}" 155 | else 156 | # Trim subcommands from each command 157 | commands[$i]="\${commands[$i]%%:*}" 158 | fi 159 | done 160 | } 161 | 162 | if [[ "$cur" != "-"* ]]; then 163 | # Command 164 | __COMP_WORDS=( "\${COMP_WORDS[@]:1}" ) 165 | 166 | # The command typed by the user but separated by colons (e.g. "mycli command subcom" -> "command:subcom") 167 | normalizedCommand="$( printf "%s" "$(join_by ":" "\${__COMP_WORDS[@]}")" )" 168 | 169 | # The command hirarchy, with colons, leading up to the last subcommand entered (e.g. "mycli com subcommand subsubcom" -> "com:subcommand:") 170 | colonPrefix="\${normalizedCommand%"\${normalizedCommand##*:}"}" 171 | 172 | if [[ -z "$normalizedCommand" ]]; then 173 | # If there is no normalizedCommand yet the user hasn't typed in a full command 174 | # So we should trim all subcommands & flags from $commands so we can suggest all top level commands 175 | opts=$(printf "%s " "\${commands[@]}" | grep -Eo '^[a-zA-Z0-9_-]+') 176 | else 177 | # Filter $commands to just the ones that match the $normalizedCommand and turn into an array 178 | commands=( $(compgen -W "$commands" -- "\${normalizedCommand}") ) 179 | # Trim higher level and subcommands from the subcommands to suggest 180 | __trim_colon_commands "$colonPrefix" 181 | 182 | opts=$(printf "%s " "\${commands[@]}") # | grep -Eo '^[a-zA-Z0-9_-]+' 183 | fi 184 | ${'else '} 185 | # Flag 186 | 187 | # The full CLI command separated by colons (e.g. "mycli command subcommand --fl" -> "command:subcommand") 188 | # This needs to be defined with $COMP_CWORD-1 as opposed to above because the current "word" on the command line is a flag and the command is everything before the flag 189 | normalizedCommand="$( printf "%s" "$(join_by ":" "\${COMP_WORDS[@]:1:($COMP_CWORD - 1)}")" )" 190 | 191 | # The line below finds the command in $commands using grep 192 | # Then, using sed, it removes everything from the found command before the --flags (e.g. "command:subcommand:subsubcom --flag1 --flag2" -> "--flag1 --flag2") 193 | opts=$(printf "%s " "\${commands[@]}" | grep "\${normalizedCommand}" | sed -n "s/^\${normalizedCommand} //p") 194 | fi 195 | 196 | COMPREPLY=($(compgen -W "$opts" -- "\${cur}")) 197 | } 198 | 199 | complete -F _oclif-example_autocomplete oclif-example\n`) 200 | }) 201 | 202 | it('#zshCompletionFunction', () => { 203 | /* eslint-disable no-useless-escape */ 204 | expect(cmd.zshCompletionFunction).to.eq(`#compdef oclif-example 205 | 206 | _oclif-example () { 207 | local _command_id=\${words[2]} 208 | local _cur=\${words[CURRENT]} 209 | local -a _command_flags=() 210 | 211 | ## public cli commands & flags 212 | local -a _all_commands=( 213 | "autocomplete:display autocomplete instructions" 214 | "autocomplete\\:foo:cmd for autocomplete testing \\\\\\\`with some potentially dangerous script\\\\\\\` and \\\\\[square brackets\\\\\] and \\\\\\\"double-quotes\\\\\\\"" 215 | "foo:cmd for autocomplete testing \\\\\\\`with some potentially dangerous script\\\\\\\` and \\\\\[square brackets\\\\\] and \\\\\\\"double-quotes\\\\\\\"" 216 | ) 217 | 218 | _set_flags () { 219 | case $_command_id in 220 | autocomplete) 221 | _command_flags=( 222 | "--skip-instructions[don't show installation instructions]" 223 | ) 224 | ;; 225 | 226 | autocomplete:foo) 227 | _command_flags=( 228 | "--bar=-[bar for testing]:" 229 | "--baz=-[baz for testing]:" 230 | "--dangerous=-[\\\\\\\`with some potentially dangerous script\\\\\\\`]:" 231 | "--brackets=-[\\\\\[square brackets\\\\\]]:" 232 | "--double-quotes=-[\\\\\\\"double-quotes\\\\\\\"]:" 233 | "--multi-line=-[multi-]:" 234 | "--json[output in json format]" 235 | ) 236 | ;; 237 | 238 | foo) 239 | _command_flags=( 240 | "--bar=-[bar for testing]:" 241 | "--baz=-[baz for testing]:" 242 | "--dangerous=-[\\\\\\\`with some potentially dangerous script\\\\\\\`]:" 243 | "--brackets=-[\\\\\[square brackets\\\\\]]:" 244 | "--double-quotes=-[\\\\\\\"double-quotes\\\\\\\"]:" 245 | "--multi-line=-[multi-]:" 246 | "--json[output in json format]" 247 | ) 248 | ;; 249 | 250 | esac 251 | } 252 | ## end public cli commands & flags 253 | 254 | _complete_commands () { 255 | _describe -t all-commands "all commands" _all_commands 256 | } 257 | 258 | if [ $CURRENT -gt 2 ]; then 259 | if [[ "$_cur" == -* ]]; then 260 | _set_flags 261 | else 262 | _path_files 263 | fi 264 | fi 265 | 266 | 267 | _arguments -S '1: :_complete_commands' \\ 268 | $_command_flags 269 | } 270 | 271 | _oclif-example\n`) 272 | 273 | /* eslint-enable no-useless-escape */ 274 | }) 275 | }) 276 | }) 277 | -------------------------------------------------------------------------------- /test/commands/autocomplete/index.test.ts: -------------------------------------------------------------------------------- 1 | import {runCommand} from '@oclif/test' 2 | import {expect} from 'chai' 3 | 4 | // autocomplete will throw error on windows ci 5 | import {default as skipWindows} from '../../helpers/runtest.js' 6 | 7 | skipWindows('autocomplete index', () => { 8 | it('provides bash instructions', async () => { 9 | const {stdout} = await runCommand('autocomplete bash') 10 | expect(stdout).to.contain(`Setup Instructions for OCLIF-EXAMPLE CLI Autocomplete ---`) 11 | }) 12 | 13 | it('provides zsh instructions', async () => { 14 | const {stdout} = await runCommand('autocomplete zsh') 15 | expect(stdout).to.contain(`Setup Instructions for OCLIF-EXAMPLE CLI Autocomplete ---`) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/autocomplete/script.test.ts: -------------------------------------------------------------------------------- 1 | import {Config} from '@oclif/core' 2 | import {runCommand} from '@oclif/test' 3 | import {expect} from 'chai' 4 | 5 | // autocomplete will throw error on windows ci 6 | import {default as skipWindows} from '../../helpers/runtest.js' 7 | 8 | skipWindows('autocomplete:script', () => { 9 | it('provides bash profile config', async () => { 10 | const config = await Config.load(import.meta.url) 11 | const {stdout} = await runCommand('autocomplete:script bash', config) 12 | expect(stdout).to.contain(` 13 | OCLIF_EXAMPLE_AC_BASH_SETUP_PATH=${config.cacheDir}/autocomplete/bash_setup && test -f $OCLIF_EXAMPLE_AC_BASH_SETUP_PATH && source $OCLIF_EXAMPLE_AC_BASH_SETUP_PATH; # oclif-example autocomplete setup 14 | `) 15 | }) 16 | 17 | it('provides zsh profile config', async () => { 18 | const config = await Config.load(import.meta.url) 19 | const {stdout} = await runCommand('autocomplete:script zsh', config) 20 | expect(stdout).to.contain(` 21 | OCLIF_EXAMPLE_AC_ZSH_SETUP_PATH=${config.cacheDir}/autocomplete/zsh_setup && test -f $OCLIF_EXAMPLE_AC_ZSH_SETUP_PATH && source $OCLIF_EXAMPLE_AC_ZSH_SETUP_PATH; # oclif-example autocomplete setup 22 | `) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/helpers/runtest.ts: -------------------------------------------------------------------------------- 1 | import {platform} from 'node:os' 2 | 3 | // eslint-disable-next-line mocha/no-exports, unicorn/no-anonymous-default-export 4 | export default (msg: string, cbk: () => void) => 5 | platform() === 'win32' ? console.log('skipping on windows') : describe(msg, cbk) 6 | -------------------------------------------------------------------------------- /test/test.oclif.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "commands": { 4 | "autocomplete": { 5 | "id": "autocomplete", 6 | "description": "display autocomplete instructions", 7 | "pluginName": "@oclif/plugin-autocomplete", 8 | "pluginType": "core", 9 | "aliases": [], 10 | "examples": [ 11 | "$ heroku autocomplete", 12 | "$ heroku autocomplete bash", 13 | "$ heroku autocomplete zsh" 14 | ], 15 | "flags": { 16 | "skip-instructions": { 17 | "name": "skip-instructions", 18 | "type": "boolean", 19 | "char": "s", 20 | "description": "don't show installation instructions" 21 | } 22 | }, 23 | "args": [ 24 | { 25 | "name": "shell", 26 | "description": "shell type", 27 | "required": false 28 | } 29 | ] 30 | }, 31 | "autocomplete:foo": { 32 | "id": "autocomplete:foo", 33 | "description": "cmd for autocomplete testing `with some potentially dangerous script` and [square brackets] and \"double-quotes\"\nand a multi-line description", 34 | "pluginName": "@oclif/plugin-autocomplete", 35 | "pluginType": "core", 36 | "aliases": ["foo"], 37 | "flags": { 38 | "bar": { 39 | "name": "bar", 40 | "type": "option", 41 | "description": "bar for testing" 42 | }, 43 | "baz": { 44 | "name": "baz", 45 | "type": "option", 46 | "description": "baz for testing" 47 | }, 48 | "dangerous": { 49 | "name": "dangerous", 50 | "type": "option", 51 | "description": "`with some potentially dangerous script`" 52 | }, 53 | "brackets": { 54 | "name": "brackets", 55 | "type": "option", 56 | "description": "[square brackets]" 57 | }, 58 | "double-quotes": { 59 | "name": "double-quotes", 60 | "type": "option", 61 | "description": "\"double-quotes\"" 62 | }, 63 | "multi-line": { 64 | "name": "multi-line", 65 | "type": "option", 66 | "description": "multi-\nline" 67 | }, 68 | "json": { 69 | "name": "json", 70 | "type": "boolean", 71 | "description": "output in json format" 72 | } 73 | }, 74 | "args": [] 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "include": ["./**/*", "../src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /test/zsh_example: -------------------------------------------------------------------------------- 1 | #compdef auto 2 | 3 | _auto () { 4 | local curcontext="$curcontext" state line 5 | typeset -A opt_args 6 | 7 | _arguments \ 8 | '1: :->country'\ 9 | '*: :->city' 10 | 11 | case $state in 12 | country) 13 | _arguments '1:Countries:(France Germany Italy)' 14 | ;; 15 | *) 16 | case $words[2] in 17 | France) 18 | compadd "$@" Paris Lyon Marseille 19 | ;; 20 | Germany) 21 | compadd "$@" Berlin Munich Dresden 22 | ;; 23 | Italy) 24 | compadd "$@" Rome Napoli Palermo 25 | ;; 26 | *) 27 | _files 28 | esac 29 | esac 30 | } 31 | 32 | _auto 33 | 34 | # local -a _command_flags=( 35 | # '--app=-[app to use]' 36 | # '--add[add to use]' 37 | # ) 38 | 39 | # _arguments ': :->command' \ 40 | # '*: :->flag' \ 41 | # $_command_flags 42 | 43 | # case $state in 44 | # command) 45 | # # echo 'command' 46 | # _describe -t all-commands "all commands" _commands 47 | # ;; 48 | # flag) 49 | # if [ $CURRENT -gt 2 ]; then 50 | # if [[ "$_cur" == -* ]]; then 51 | # _complete_flags 52 | # fi 53 | # fi 54 | # ;; 55 | # esac 56 | 57 | # _auto () { 58 | # local _command_id=${words[2]} 59 | # local _cur=${words[CURRENT]} 60 | # local -a _command_flags=() 61 | # 62 | # _arguments '*:: :->command' 63 | # # '*: :->flag' 64 | # _arguments '2: :->flag' 65 | # 66 | # case $state in 67 | # command) 68 | # echo 'command' 69 | # # _describe -t all-fcommands "all commands" _commands 70 | # ;; 71 | # flag) 72 | # echo 'flag' 73 | # ;; 74 | # esac 75 | # } 76 | 77 | # _arguments -S '1:all commands:_complete_commands' 78 | 79 | # _arguments -S '1: :_complete_commands' \ 80 | # "--name=-[name to use]:name:(boo far)" \ 81 | # "--bame=-[bame to use]:" 82 | 83 | # case $state in 84 | # name) 85 | # compadd "$@" boo far 86 | # ;; 87 | # esac 88 | 89 | # _arguments '-l+:left border:' \ 90 | # '-format:paper size:(letter A4)' \ 91 | # '*-copy:output file:_files::resolution:(300 600)' \ 92 | # ':postscript file:_files -g \*.\(ps\|eps\)' \ 93 | # '*:page number:' 94 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "Node16", 6 | "outDir": "./lib", 7 | "pretty": true, 8 | "rootDirs": ["./src"], 9 | "strict": true, 10 | "target": "ES2022", 11 | "moduleResolution": "Node16" 12 | }, 13 | "include": ["./src/**/*"], 14 | "ts-node": { 15 | "esm": true 16 | } 17 | } 18 | --------------------------------------------------------------------------------