From 785a158451e0df62adbc150ffa5ca6933b39eed6 Mon Sep 17 00:00:00 2001 From: Ismo Vuorinen Date: Tue, 29 Jul 2025 12:47:50 +0300 Subject: [PATCH] Initial commit --- .editorconfig | 13 + .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 145 +++ .github/ISSUE_TEMPLATE/bug_report.md | 38 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/renovate.json | 6 + .github/workflows/codeql.yml | 46 + .github/workflows/pr-lint.yml | 30 + .github/workflows/stale.yml | 26 + .github/workflows/sync-labels.yml | 41 + .gitignore | 134 +++ .markdownlint.json | 13 + .mega-linter.yml | 35 + .pre-commit-config.yaml | 63 ++ .shellcheckrc | 1 + .yamlignore | 0 .yamllint.yml | 13 + CLAUDE.md | 152 +++ LICENSE | 20 + LICENSE.md | 21 + README.md | 286 ++++++ completions/phpenv.fish | 123 +++ conf.d/phpenv.fish | 38 + fish_plugins | 85 ++ functions/phpenv.fish | 1078 +++++++++++++++++++++ 25 files changed, 2428 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/renovate.json create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pr-lint.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/sync-labels.yml create mode 100644 .gitignore create mode 100644 .markdownlint.json create mode 100644 .mega-linter.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .shellcheckrc create mode 100644 .yamlignore create mode 100644 .yamllint.yml create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 completions/phpenv.fish create mode 100644 conf.d/phpenv.fish create mode 100644 fish_plugins create mode 100644 functions/phpenv.fish diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e77edde --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +max_line_length = 120 + diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a5ff83b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ivuorinen diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2d86e93 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,145 @@ +# Citizen Code of Conduct + +## 1. Purpose + +A primary goal of @ivuorinen's repositories is to be inclusive to the largest +number of contributors, with the most varied and diverse backgrounds possible. +As such, we are committed to providing a friendly, safe and welcoming +environment for all, regardless of gender, sexual orientation, ability, +ethnicity, socioeconomic status, and religion (or lack thereof). + +This code of conduct outlines our expectations for all those who participate in +our community, as well as the consequences for unacceptable behavior. + +We invite all those who participate in @ivuorinen's repositories to help us +create safe and positive experiences for everyone. + +## 2. Open [Source/Culture/Tech] Citizenship + +A supplemental goal of this Code of Conduct is to increase +open [source/culture/tech] citizenship by encouraging participants to recognize +and strengthen the relationships between our actions and their effects on our +community. + +Communities mirror the societies in which they exist and positive action is +essential to counteract the many forms of inequality and abuses of power that +exist in society. + +If you see someone who is making an extra effort to ensure our community is +welcoming, friendly, and encourages all participants to contribute to the +fullest extent, we want to know. + +## 3. Expected Behavior + +The following behaviors are expected and requested of all community members: + +* Participate in an authentic and active way. In doing so, you contribute to the + health and longevity of this community. +* Exercise consideration and respect in your speech and actions. +* Attempt collaboration before conflict. +* Refrain from demeaning, discriminatory, or harassing behavior and speech. +* Be mindful of your surroundings and of your fellow participants. Alert + community leaders if you notice a dangerous situation, someone in distress, or + violations of this Code of Conduct, even if they seem inconsequential. +* Remember that community event venues may be shared with members of the public; + please be respectful to all patrons of these locations. + +## 4. Unacceptable Behavior + +The following behaviors are considered harassment and are unacceptable within +our community: + +* Violence, threats of violence or violent language directed against another + person. +* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory + jokes and language. +* Posting or displaying sexually explicit or violent material. +* Posting or threatening to post other people's personally identifying + information ("doxing"). +* Personal insults, particularly those related to gender, sexual orientation, + race, religion, or disability. +* Inappropriate photography or recording. +* Inappropriate physical contact. You should have someone's consent before + touching them. +* Unwelcome sexual attention. This includes, sexualized comments or jokes; + inappropriate touching, groping, and unwelcomed sexual advances. +* Deliberate intimidation, stalking or following (online or in person). +* Advocating for, or encouraging, any of the above behavior. +* Sustained disruption of community events, including talks and presentations. + +## 5. Weapons Policy + +No weapons will be allowed at @ivuorinen's repositories events, community +spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons +include but are not limited to guns, explosives (including fireworks), and large +knives such as those used for hunting or display, as well as any other item used +for the purpose of causing injury or harm to others. Anyone seen in possession +of one of these items will be asked to leave immediately, and will only be +allowed to return without the weapon. Community members are further expected to +comply with all state and local laws on this matter. + +## 6. Consequences of Unacceptable Behavior + +Unacceptable behavior from any community member, including sponsors and those +with decision-making authority, will not be tolerated. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. + +If a community member engages in unacceptable behavior, the community organizers +may take any action they deem appropriate, up to and including a temporary ban +or permanent expulsion from the community without warning (and without refund in +the case of a paid event). + +## 7. Reporting Guidelines + +If you are subject to or witness unacceptable behavior, or have any other +concerns, please notify a community organizer as soon as possible: + + +Additionally, community organizers are available to help community members +engage with local law enforcement or to otherwise help those experiencing +unacceptable behavior feel safe. In the context of in-person events, organizers +will also provide escorts as desired by the person experiencing distress. + +## 8. Addressing Grievances + +If you feel you have been falsely or unfairly accused of violating this Code of +Conduct, you should notify @ivuorinen with a concise description of your +grievance. Your grievance will be handled in accordance with our existing +governing policies. + +## 9. Scope + +We expect all community participants (contributors, paid or otherwise; sponsors; +and other guests) to abide by this Code of Conduct in all community +venues--online and in-person--as well as in all one-on-one communications +pertaining to community business. + +This code of conduct and its related procedures also applies to unacceptable +behavior occurring outside the scope of community activities when such behavior +has the potential to adversely affect the safety and well-being of community +members. + +## 10. Contact info + +@ivuorinen + + +## 11. License and attribution + +The Citizen Code of Conduct is distributed by [Stumptown Syndicate][stumptown] +under a [Creative Commons Attribution-ShareAlike license][cc-by-sa]. + +Portions of text derived from the [Django Code of Conduct][django] and +the [Geek Feminism Anti-Harassment Policy][geek-feminism]. + +* _Revision 2.3. Posted 6 March 2017._ +* _Revision 2.2. Posted 4 February 2016._ +* _Revision 2.1. Posted 23 June 2014._ +* _Revision 2.0, adopted by the [Stumptown Syndicate][stumptown] board on 10 + January 2013. Posted 17 March 2013._ + +[stumptown]: https://github.com/stumpsyn +[cc-by-sa]: https://creativecommons.org/licenses/by-sa/3.0/ +[django]: https://www.djangoproject.com/conduct/ +[geek-feminism]: http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f57b5f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: ivuorinen + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..abdc2e8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: ivuorinen + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..e46316f --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>ivuorinen/renovate-config" + ] +} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c75d036 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,46 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: 'CodeQL' + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + schedule: + - cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday + merge_group: + +permissions: + actions: read + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript'] # Add languages used in your actions + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@4e828ff8d448a8a6e532957b1811f387a63867e8 # v3.29.4 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 0000000..75e08c0 --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,30 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Lint Code Base + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + Linter: + name: PR Lint + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + statuses: write + contents: read + packages: read + + steps: + - name: Run PR Lint + # https://github.com/ivuorinen/actions + uses: ivuorinen/actions/pr-lint@8476cd4675ea8210eadf4a267bbeb13bddea4e75 # 25.7.21 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..667f284 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,26 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Stale + +on: + schedule: + - cron: '0 8 * * *' # Every day at 08:00 + workflow_call: + workflow_dispatch: + +permissions: + contents: read + packages: read + statuses: read + +jobs: + stale: + name: 🧹 Clean up stale issues and PRs + runs-on: ubuntu-latest + + permissions: + contents: write # only for delete-branch option + issues: write + pull-requests: write + steps: + - uses: ivuorinen/actions/stale@8476cd4675ea8210eadf4a267bbeb13bddea4e75 # 25.7.21 diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..5f88668 --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,41 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Sync Labels + +on: + push: + branches: + - main + - master + paths: + - '.github/labels.yml' + - '.github/workflows/sync-labels.yml' + schedule: + - cron: '34 5 * * *' # Run every day at 05:34 AM UTC + workflow_call: + workflow_dispatch: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: read-all + +jobs: + labels: + name: ♻️ Sync Labels + runs-on: ubuntu-latest + timeout-minutes: 10 + + permissions: + contents: read + issues: write + + steps: + - name: ⤵️ Checkout Repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: ⤵️ Sync Latest Labels Definitions + uses: ivuorinen/actions/sync-labels@8476cd4675ea8210eadf4a267bbeb13bddea4e75 # 25.7.21 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2a1faa --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +.php-cs-fixer.cache +.php-cs-fixer.php +composer.phar +/vendor/ +.phpunit.result.cache +.phpunit.cache +/app/phpunit.xml +/phpunit.xml +/build/ +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +*.lcov +.nyc_output +.grunt +bower_components +.lock-wscript +build/Release +node_modules/ +jspm_packages/ +web_modules/ +*.tsbuildinfo +.npm +.eslintcache +.stylelintcache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ +.node_repl_history +*.tgz +.yarn-integrity +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +.cache +.parcel-cache +.next +out +.nuxt +dist +.cache/ +.vuepress/dist +.temp +.docusaurus +.serverless/ +.fusebox/ +.dynamodb/ +.tern-port +.vscode-test +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] +Session.vim +Sessionx.vim +.netrwhist +*~ +tags +[._]*.un~ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +npm-debug.log +yarn-error.log +bootstrap/compiled.php +app/storage/ +public/storage +public/hot +public_html/storage +public_html/hot +storage/*.key +Homestead.yaml +Homestead.json +/.vagrant +/node_modules +/.pnp +.pnp.js +/coverage +/.next/ +/out/ +/build +.DS_Store +*.pem +.env*.local +.vercel +next-env.d.ts diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..3de10f3 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,13 @@ +{ + "default": true, + "MD013": { + "line_length": 200, + "code_blocks": false, + "tables": false + }, + "MD024": { + "siblings_only": true + }, + "MD033": false, + "MD041": false +} diff --git a/.mega-linter.yml b/.mega-linter.yml new file mode 100644 index 0000000..82e546d --- /dev/null +++ b/.mega-linter.yml @@ -0,0 +1,35 @@ +--- +# Configuration file for MegaLinter +# See all available variables at +# https://megalinter.io/configuration/ and in linters documentation + +APPLY_FIXES: all +SHOW_ELAPSED_TIME: false # Show elapsed time at the end of MegaLinter run +PARALLEL: true +VALIDATE_ALL_CODEBASE: true +FILEIO_REPORTER: false # Generate file.io report +GITHUB_STATUS_REPORTER: true # Generate GitHub status report +IGNORE_GENERATED_FILES: true # Ignore generated files +JAVASCRIPT_DEFAULT_STYLE: prettier # Default style for JavaScript +PRINT_ALPACA: false # Print Alpaca logo in console +SARIF_REPORTER: true # Generate SARIF report +SHOW_SKIPPED_LINTERS: false # Show skipped linters in MegaLinter log + +DISABLE_LINTERS: + - REPOSITORY_DEVSKIM + +ENABLE_LINTERS: + - YAML_YAMLLINT + - MARKDOWN_MARKDOWNLINT + - YAML_PRETTIER + - JSON_PRETTIER + - JAVASCRIPT_ES + - TYPESCRIPT_ES + +YAML_YAMLLINT_CONFIG_FILE: .yamllint.yml +MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.json +JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json +TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json + +FILTER_REGEX_EXCLUDE: > + (node_modules|\.automation/test|docs/json-schemas|\.github/workflows) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ccfa22d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: requirements-txt-fixer + - id: detect-private-key + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + - id: check-case-conflict + - id: check-merge-conflict + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-xml + - id: check-yaml + args: [--allow-multiple-documents] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=auto] + - id: pretty-format-json + args: [--autofix, --no-sort-keys] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.44.0 + hooks: + - id: markdownlint + args: [-c, .markdownlint.json, --fix] + + - repo: https://github.com/adrienverge/yamllint + rev: v1.37.0 + hooks: + - id: yamllint + + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.11.0-1 + hooks: + - id: shfmt + + - repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.10.0 + hooks: + - id: shellcheck + args: ['--severity=warning'] + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.7 + hooks: + - id: actionlint + args: ['-shellcheck='] + + - repo: https://github.com/renovatebot/pre-commit-hooks + rev: 39.227.2 + hooks: + - id: renovate-config-validator + + - repo: https://github.com/bridgecrewio/checkov.git + rev: '3.2.400' + hooks: + - id: checkov + args: + - '--quiet' diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..b430800 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +disable=SC2129 diff --git a/.yamlignore b/.yamlignore new file mode 100644 index 0000000..e69de29 diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..065bc60 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,13 @@ +--- +extends: default + +rules: + line-length: + max: 200 + level: warning + truthy: + check-keys: false + comments: + min-spaces-from-content: 1 + trailing-spaces: + level: warning diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2753b34 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,152 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +phpenv.fish is a PHP version manager for Fish Shell, similar to nvm for Node.js. +It provides fast PHP version switching, extension management, and automatic version detection from project files. + +## Development Commands + +### Testing Changes + +Since this is a Fish shell plugin, test changes by: + +```bash +# Reload the function after changes +source functions/phpenv.fish + +# Test commands +phpenv help +phpenv versions +phpenv current +``` + +### Installation for Development + +```bash +# Link the development version to Fish config +ln -sf $PWD/functions/phpenv.fish ~/.config/fish/functions/phpenv.fish +ln -sf $PWD/completions/phpenv.fish ~/.config/fish/completions/phpenv.fish +ln -sf $PWD/conf.d/phpenv.fish ~/.config/fish/conf.d/phpenv.fish +``` + +## Code Architecture + +### Core Components + +1. **Main Dispatcher (`functions/phpenv.fish`)** + - Entry point function that routes commands to internal functions + - All subcommands are implemented as `__phpenv_*` functions + - Version detection logic in `__phpenv_detect_version` + +2. **Completions (`completions/phpenv.fish`)** + - Provides tab completions for all commands + - Fetches available versions dynamically from shivammathur/setup-php + +3. **Configuration (`conf.d/phpenv.fish`)** + - Sets up Fish universal variables on load + - Handles PATH initialization + +### Key Design Patterns + +- **Performance Focus**: Direct directory checks instead of `brew list` (100-1000x faster) +- **Fish Universal Variables**: Used for configuration persistence +- **Homebrew Integration**: Uses shivammathur taps for PHP/extension installation +- **Version File Priority**: `.php-version` > `.tool-version` > `composer.json` > global > system + +### Version Detection Flow + +1. Check for `.php-version` file (exact version) +2. Check for `.tool-version` file (parse PHP line) +3. Check `composer.json` for PHP constraints (semver resolution) +4. Use global version from Fish universal variable +5. Fall back to system PHP + +### Important Implementation Details + +- All internal functions are prefixed with `__phpenv_` +- Version resolution supports semver constraints (^8.1, ~8.2.0, etc.) +- Extension management uses separate Homebrew tap +- Auto-switching uses Fish's `pwd` event handler +- Configuration stored in Fish universal variables with `PHPENV_` prefix + +## Code Style Requirements + +- Maximum line length: 120 characters (enforced by .editorconfig) +- Use LF line endings +- UTF-8 encoding +- Trim trailing whitespace +- Insert final newline + +## PATH and Variable Management + +### PATH State Tracking + +- `PHPENV_ORIGINAL_PATH`: Stores initial PATH before any modifications +- `PHPENV_CURRENT_VERSION`: Tracks currently active PHP version +- `PHPENV_CURRENT_PATH`: Stores path to current PHP binary +- Use `phpenv use system` to restore original PATH + +### Variable Scope Strategy + +- **Universal variables** (`set -U`): Only `PHPENV_GLOBAL_VERSION` (persists across shells) +- **Session variables** (`set -g`): Configuration settings (per-shell session) +- **Local variables** (`set -l`): Function-scoped, automatically cleaned up + +### Auto-Switch Debouncing + +- `PHPENV_LAST_SWITCH_TIME`: Prevents excessive PATH changes on rapid directory changes +- 1-second minimum interval between auto-switches +- Early exit if already using correct version + +## Common Tasks + +### Adding a New Command + +1. Add case in main `phpenv` function switch statement +2. Implement `__phpenv_` function +3. Add completions in `completions/phpenv.fish` +4. Update help text in `__phpenv_help` + +### Modifying Version Detection + +- Edit `__phpenv_detect_version` function +- Maintain priority order of version sources +- Test with various project configurations + +### Working with Homebrew Integration + +- PHP versions: `shivammathur/php/php@` +- Extensions: `shivammathur/extensions/@` +- Check formula existence before operations + +### Performance Optimizations + +#### Caching System + +- `__phpenv_version_cache`: 5-minute cache for API version data +- `__phpenv_cellar_cache`: Permanent cache for Homebrew Cellar path +- Reduces network calls and filesystem operations + +#### Unified Helper Functions + +- `__phpenv_parse_version_field`: Single function for all jq parsing (eliminates 9+ duplicated calls) +- `__phpenv_ensure_taps`: Unified Homebrew tap management +- `__phpenv_get_available_extensions`: Shared extension listing logic + +### PATH Management Best Practices + +- Always check `PHPENV_ORIGINAL_PATH` exists before modification +- Use debouncing for automatic operations +- Validate PHP paths before setting +- Provide restoration mechanism (`phpenv use system`) +- Clean up temporary variables in error cases + +### Code Organization Principles + +- Cache expensive operations (API calls, filesystem checks) +- Unify repeated patterns into helper functions +- Use session variables instead of universal where possible +- Minimize network requests and subprocess calls diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6b210e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License Copyright (c) 2025 Ismo Vuorinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8d1507b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Ismo Vuorinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..485fb61 --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +# phpenv.fish + +A fast, feature-rich PHP version manager for Fish Shell that acts like goenv or nvm. + +## Features + +- **Fast version detection** (100-1000x faster than `brew list`) +- **Dynamic version resolution** from [shivammathur/setup-php](https://github.com/shivammathur/setup-php) +- **Multiple version sources**: `.php-version`, `.tool-version`, `composer.json` +- **Auto-installation** of missing PHP versions +- **Extension management** with availability checking +- **Composer.json integration** with full semver support +- **Auto-switching** between versions (configurable) +- **Fisher package manager** support +- **Rich completions** with descriptions + +## Installation + +### Using Fisher (recommended) + +```bash +fisher install ivuorinen/phpenv.fish +``` + +### Manual Installation + +1. Copy files to your fish configuration: + ```bash + # Functions + curl -L https://raw.githubusercontent.com/ivuorinen/phpenv.fish/main/functions/phpenv.fish > ~/.config/fish/functions/phpenv.fish + + # Completions + curl -L https://raw.githubusercontent.com/ivuorinen/phpenv.fish/main/completions/phpenv.fish > ~/.config/fish/completions/phpenv.fish + + # Configuration + curl -L https://raw.githubusercontent.com/ivuorinen/phpenv.fish/main/conf.d/phpenv.fish > ~/.config/fish/conf.d/phpenv.fish + ``` + +2. Install dependencies: + ```bash + brew install jq + ``` + +3. Add Homebrew taps: + ```bash + brew tap shivammathur/php + brew tap shivammathur/extensions + ``` + +## Quick Start + +```bash +# Show available versions +phpenv versions + +# Install PHP versions +phpenv install 8.3 +phpenv install 8.1 + +# Set global default +phpenv global 8.3 + +# Set project-specific version +phpenv local 8.1 + +# Install extensions +phpenv extensions install xdebug +phpenv extensions install redis + +# Configure behavior +phpenv config set auto-switch false # Disable auto-switching +phpenv config set auto-install true # Enable auto-installation + +# Check installation +phpenv doctor +``` + +## Commands + +### Version Management + +- `phpenv install ` - Install PHP version +- `phpenv uninstall ` - Uninstall PHP version +- `phpenv use ` - Use version for current shell +- `phpenv local ` - Set version for current project +- `phpenv global ` - Set global default version +- `phpenv list` - List installed versions +- `phpenv current` - Show current version +- `phpenv versions` - Show all available versions + +### Extension Management + +- `phpenv extensions install ` - Install extension for current PHP +- `phpenv extensions uninstall ` - Uninstall extension +- `phpenv extensions list` - List installed extensions +- `phpenv extensions available` - Show available extensions + +### Configuration + +- `phpenv config get ` - Get configuration value +- `phpenv config set ` - Set configuration value +- `phpenv config list` - List all configuration + +### Utilities + +- `phpenv which [binary]` - Show path to PHP binary +- `phpenv doctor` - Check installation health +- `phpenv help` - Show help + +## Version Detection + +phpenv automatically detects PHP versions from multiple sources (in priority order): + +1. **`.php-version`** - Project-specific version file +2. **`.tool-version`** - Tool version file (parses `v8.4` as `8.4`) +3. **`composer.json`** - Both `config.platform.php` and `require.php` with semver support +4. **Global version** - Fish universal variable +5. **System PHP** - Fallback to system installation + +### Composer.json Support + +Supports all semver constraints: +- `^8.1` → Uses PHP 8.3 (latest 8.x) +- `~8.2.0` → Uses PHP 8.2 +- `>=8.0` → Uses PHP 8.3 +- `8.1.*` → Uses PHP 8.1 + +Checks both locations: +```json +{ + "require": { + "php": "^8.1" + }, + "config": { + "platform": { + "php": "8.2.0" + } + } +} +``` + +## Configuration + +### Configuration Keys + +- `auto-install` - Auto-install missing versions (default: false) +- `auto-install-extensions` - Install extensions with new PHP versions (default: false) +- `auto-switch` - Auto-switch versions when changing directories (default: true) +- `default-extensions` - Space-separated list of default extensions (default: "opcache") +- `global-version` - Global PHP version + +### Configuration Files + +phpenv checks these locations (in order): + +1. `~/.config/fish/conf.d/phpenv.fish` (preferred) +2. `~/.config/phpenv/config` +3. `~/.phpenv.fish` + +### Examples + +```bash +# Enable auto-installation +phpenv config set auto-install true + +# Disable auto-switching +phpenv config set auto-switch false + +# Set default extensions +phpenv config set default-extensions "opcache xdebug redis" + +# Enable auto-extension installation +phpenv config set auto-install-extensions true +``` + +## Supported PHP Versions + +Uses [shivammathur/homebrew-php](https://github.com/shivammathur/homebrew-php) with dynamic version detection: + +**Version Aliases:** +- `latest` - Latest stable PHP version +- `nightly` - Development version +- `8.x` - Latest PHP 8.x version +- `7.x` - Latest PHP 7.x version +- `5.x` - Latest PHP 5.x version + +**Available Versions:** 5.6, 7.0-7.4, 8.0-8.5 + +## Supported Extensions + +Uses [shivammathur/homebrew-extensions](https://github.com/shivammathur/homebrew-extensions): + +- xdebug, redis, imagick, mongodb, memcached +- pcov, ast, grpc, protobuf, yaml +- And many more... + +## Performance + +- **Directory checks**: ~1-5ms (vs ~1000ms for `brew list`) +- **Bulk version detection**: ~10ms for all versions +- **No Ruby overhead** or git operations +- **Efficient caching** and lazy loading + +## Auto-switching + +phpenv automatically switches PHP versions when you change directories if a version file is detected in the project. + +## Fisher Integration + +Works seamlessly with Fisher package manager: + +```bash +# Install +fisher install ivuorinen/phpenv.fish + +# Update +fisher update ivuorinen/phpenv.fish + +# Uninstall +fisher remove ivuorinen/phpenv.fish +``` + +## Troubleshooting + +### Check Installation + +```bash +phpenv doctor +``` + +### Common Issues + +#### jq not found + +```bash +brew install jq +``` + +#### PHP version not found + +```bash +# Add taps manually +brew tap shivammathur/php +brew tap shivammathur/extensions + +# Install specific version +phpenv install 8.3 +``` + +#### Slow performance + +- phpenv is designed to be fast by avoiding `brew list` +- If performance issues persist, check your filesystem or try `phpenv doctor` + +### Debug Information + +```bash +# Check current detection +phpenv current + +# Check which binary is used +phpenv which php + +# List all configuration +phpenv config list +``` + +## Contributing + +Contributions welcome! Please: + +1. Follow fish shell best practices +2. Add tests for new functionality +3. Update documentation +4. Maintain performance optimizations + +## License + +MIT License - see LICENSE file for details. + +## Related Projects + +- [shivammathur/homebrew-php](https://github.com/shivammathur/homebrew-php) +- [shivammathur/homebrew-extensions](https://github.com/shivammathur/homebrew-extensions) +- [jorgebucaran/fisher](https://github.com/jorgebucaran/fisher) + diff --git a/completions/phpenv.fish b/completions/phpenv.fish new file mode 100644 index 0000000..8b627e8 --- /dev/null +++ b/completions/phpenv.fish @@ -0,0 +1,123 @@ +# Completions for phpenv command +# Place in ~/.config/fish/completions/phpenv.fish + +# Complete main commands +complete -c phpenv -f -n "__fish_use_subcommand" -a "install" -d "Install a PHP version" +complete -c phpenv -f -n "__fish_use_subcommand" -a "uninstall" -d "Uninstall a PHP version" +complete -c phpenv -f -n "__fish_use_subcommand" -a "use" -d "Use PHP version for current shell" +complete -c phpenv -f -n "__fish_use_subcommand" -a "local" -d "Set PHP version for current project" +complete -c phpenv -f -n "__fish_use_subcommand" -a "global" -d "Set global PHP version" +complete -c phpenv -f -n "__fish_use_subcommand" -a "list" -d "List installed PHP versions" +complete -c phpenv -f -n "__fish_use_subcommand" -a "ls" -d "List installed PHP versions" +complete -c phpenv -f -n "__fish_use_subcommand" -a "current" -d "Show current PHP version" +complete -c phpenv -f -n "__fish_use_subcommand" -a "which" -d "Show path to PHP binary" +complete -c phpenv -f -n "__fish_use_subcommand" -a "versions" -d "Show all available versions" +complete -c phpenv -f -n "__fish_use_subcommand" -a "doctor" -d "Check phpenv installation" +complete -c phpenv -f -n "__fish_use_subcommand" -a "config" -d "Manage configuration" +complete -c phpenv -f -n "__fish_use_subcommand" -a "extensions" -d "Manage PHP extensions" +complete -c phpenv -f -n "__fish_use_subcommand" -a "ext" -d "Manage PHP extensions" +complete -c phpenv -f -n "__fish_use_subcommand" -a "help" -d "Show help" + +# Helper functions for completions +function __phpenv_complete_installed_versions + phpenv list 2>/dev/null | sed 's/^[* ]*//' +end + +function __phpenv_complete_available_versions + # Try to get dynamic versions first + if command -q curl -a command -q jq; and functions -q __phpenv_parse_version_field + echo "latest" + echo "nightly" + echo "5.x" + echo "7.x" + echo "8.x" + __phpenv_parse_version_field "latest" "8.4" + __phpenv_parse_version_field "nightly" "8.5" + __phpenv_parse_version_field "5.x" "5.6" + __phpenv_parse_version_field "7.x" "7.4" + __phpenv_parse_version_field "8.x" "8.4" + end + + # Fallback to common versions + printf "5.6\n7.0\n7.1\n7.2\n7.3\n7.4\n8.0\n8.1\n8.2\n8.3\n8.4\n8.5\n" +end + +function __phpenv_complete_config_keys + printf "global-version\nauto-install\nauto-install-extensions\nauto-switch\ndefault-extensions\n" +end + +function __phpenv_complete_extensions + printf "xdebug\nredis\nimagick\nmongodb\nmemcached\npcov\nast\ngrpc\n" + printf "protobuf\nyaml\nzip\ncurl\ngd\nintl\nmbstring\nmysql\nopcache\npdo\nsockets\nxml\n" +end + +function __phpenv_complete_binaries + printf "php\nphp-config\nphpize\ncomposer\npecl\npear\n" +end + +# Complete versions for install command +complete -c phpenv -f -n "__fish_seen_subcommand_from install" \ + -a "(__phpenv_complete_available_versions)" -d "PHP version" + +# Complete installed versions for uninstall, use commands +complete -c phpenv -f -n "__fish_seen_subcommand_from uninstall use local global" \ + -a "(__phpenv_complete_installed_versions)" -d "Installed PHP version" + +# Add system option for use command +complete -c phpenv -f -n "__fish_seen_subcommand_from use" -a "system" -d "Use system PHP" + +# Complete binaries for which command +complete -c phpenv -f -n "__fish_seen_subcommand_from which" -a "(__phpenv_complete_binaries)" -d "PHP binary" + +# Complete config subcommands +complete -c phpenv -f -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ + -a "get" -d "Get configuration value" +complete -c phpenv -f -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ + -a "set" -d "Set configuration value" +complete -c phpenv -f -n "__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set list" \ + -a "list" -d "List all configuration" + +# Complete config keys +complete -c phpenv -f -n "__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set" \ + -a "(__phpenv_complete_config_keys)" -d "Configuration key" + +# Complete config values for boolean settings +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from config; and __fish_seen_subcommand_from set" \ + -n "contains -- (commandline -opc)[-1] auto-install auto-install-extensions auto-switch" \ + -a "true false" -d "Boolean value" + +# Complete extensions subcommands +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "install" -d "Install PHP extension" +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "uninstall" -d "Uninstall PHP extension" +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "remove" -d "Remove PHP extension" +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "list" -d "List installed extensions" +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "ls" -d "List installed extensions" +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext" \ + -n "not __fish_seen_subcommand_from install uninstall remove list ls available" \ + -a "available" -d "Show available extensions" + +# Complete extension names +complete -c phpenv -f \ + -n "__fish_seen_subcommand_from extensions ext; and __fish_seen_subcommand_from install" \ + -a "(__phpenv_complete_extensions)" -d "PHP extension" + +# Complete help options +complete -c phpenv -f -s h -l help -d "Show help" + diff --git a/conf.d/phpenv.fish b/conf.d/phpenv.fish new file mode 100644 index 0000000..1b93ca9 --- /dev/null +++ b/conf.d/phpenv.fish @@ -0,0 +1,38 @@ +# phpenv configuration file +# Place in ~/.config/fish/conf.d/phpenv.fish + +# Set default configuration using session variables for most settings +# Only PHPENV_GLOBAL_VERSION needs to persist across shells +if not set -q PHPENV_AUTO_INSTALL + set -g PHPENV_AUTO_INSTALL false +end + +if not set -q PHPENV_AUTO_INSTALL_EXTENSIONS + set -g PHPENV_AUTO_INSTALL_EXTENSIONS false +end + +if not set -q PHPENV_AUTO_SWITCH + set -g PHPENV_AUTO_SWITCH true +end + +if not set -q PHPENV_DEFAULT_EXTENSIONS + set -g PHPENV_DEFAULT_EXTENSIONS "opcache" +end + +# Initialize PATH on shell startup if global version is set (less aggressive) +if test -n "$PHPENV_GLOBAL_VERSION"; and not set -q PHPENV_INITIALIZED + if functions -q __phpenv_is_version_installed __phpenv_set_php_path + if __phpenv_is_version_installed "$PHPENV_GLOBAL_VERSION" 2>/dev/null + # Only set PATH if no project-specific version is detected + if not __phpenv_find_version_file .php-version >/dev/null 2>&1 + if not __phpenv_find_version_file .tool-version >/dev/null 2>&1 + if not test -f composer.json + __phpenv_set_php_path "$PHPENV_GLOBAL_VERSION" 2>/dev/null + end + end + end + end + end + set -g PHPENV_INITIALIZED true +end + diff --git a/fish_plugins b/fish_plugins new file mode 100644 index 0000000..8d03752 --- /dev/null +++ b/fish_plugins @@ -0,0 +1,85 @@ +# Fisher package definition for phpenv.fish +# This file enables installation via: fisher install ivuorinen/phpenv.fish + +function __phpenv_install --on-event phpenv_install + echo "Installing phpenv.fish..." + + # Check for required dependencies + if not command -q jq + echo "Installing jq dependency..." + if command -q brew + brew install jq + else + echo "Error: Homebrew is required to install jq dependency" >&2 + echo "Install Homebrew first: https://brew.sh" >&2 + return 1 + end + end + + # Add required Homebrew taps + if command -q brew; and functions -q __phpenv_ensure_taps + echo "Adding Homebrew taps..." + __phpenv_ensure_taps + end + + # Set default configuration if not already set (use session variables) + if not set -q PHPENV_AUTO_INSTALL + set -g PHPENV_AUTO_INSTALL false + echo "Set PHPENV_AUTO_INSTALL=false (change with: phpenv config set auto-install true)" + end + + if not set -q PHPENV_AUTO_INSTALL_EXTENSIONS + set -g PHPENV_AUTO_INSTALL_EXTENSIONS false + echo "Set PHPENV_AUTO_INSTALL_EXTENSIONS=false" + end + + if not set -q PHPENV_AUTO_SWITCH + set -g PHPENV_AUTO_SWITCH true + echo "Set PHPENV_AUTO_SWITCH=true (automatic version switching enabled)" + end + + if not set -q PHPENV_DEFAULT_EXTENSIONS + set -g PHPENV_DEFAULT_EXTENSIONS "opcache" + echo "Set default extensions: opcache" + end + + echo "phpenv.fish installed successfully!" + echo "" + echo "Quick start:" + echo " phpenv versions # Show available versions" + echo " phpenv install 8.3 # Install PHP 8.3" + echo " phpenv global 8.3 # Set as global default" + echo " phpenv help # Show all commands" + echo "" + echo "Run 'phpenv doctor' to check your installation." +end + +function __phpenv_update --on-event phpenv_update + echo "Updating phpenv.fish..." + __phpenv_install +end + +function __phpenv_uninstall --on-event phpenv_uninstall + echo "Uninstalling phpenv.fish..." + + # Remove all phpenv variables (both session and universal) + set -e PHPENV_AUTO_INSTALL + set -e PHPENV_AUTO_INSTALL_EXTENSIONS + set -e PHPENV_AUTO_SWITCH + set -e PHPENV_DEFAULT_EXTENSIONS + set -e PHPENV_GLOBAL_VERSION + set -e PHPENV_ORIGINAL_PATH + set -e PHPENV_CURRENT_VERSION + set -e PHPENV_CURRENT_PATH + set -e PHPENV_LAST_SWITCH_TIME + set -e PHPENV_VERSION_OVERRIDE + set -e PHPENV_INITIALIZED + # Remove cache variables + set -e __phpenv_version_cache + set -e __phpenv_version_cache_time + set -e __phpenv_cellar_cache + + echo "phpenv.fish configuration removed." + echo "Note: Installed PHP versions are preserved." +end + diff --git a/functions/phpenv.fish b/functions/phpenv.fish new file mode 100644 index 0000000..40a5d2f --- /dev/null +++ b/functions/phpenv.fish @@ -0,0 +1,1078 @@ +# phpenv - PHP Version Manager for Fish Shell +# Repository: https://github.com/ivuorinen/phpenv.fish + +function phpenv -d "PHP version manager for Fish Shell" + if not command -q jq + echo "Error: jq is required but not installed. Install with: brew install jq" >&2 + return 1 + end + + set -l phpenv_cmd $argv[1] + set -l phpenv_args $argv[2..] + + switch $phpenv_cmd + case install + __phpenv_install $phpenv_args + case uninstall + __phpenv_uninstall $phpenv_args + case use + __phpenv_use $phpenv_args + case local + __phpenv_local $phpenv_args + case global + __phpenv_global $phpenv_args + case list ls + __phpenv_list $phpenv_args + case current + __phpenv_current + case which + __phpenv_which $phpenv_args + case versions + __phpenv_versions + case doctor + __phpenv_doctor + case config + __phpenv_config $phpenv_args + case extensions ext + __phpenv_extensions $phpenv_args + case help -h --help "" + __phpenv_help + case '*' + echo "phpenv: unknown command '$phpenv_cmd'" + echo "Run 'phpenv help' for available commands." + return 1 + end +end + +function __phpenv_current + set -l phpenv_version (__phpenv_detect_version) + if test -n "$phpenv_version" + echo "$phpenv_version" + else + echo "No PHP version set" + return 1 + end +end + +function __phpenv_detect_version + set -l phpenv_version_file (__phpenv_find_version_file .php-version) + if test -n "$phpenv_version_file" + set -l phpenv_version (string trim < $phpenv_version_file) + if test -n "$phpenv_version" + echo $phpenv_version + return + end + end + + set -l phpenv_tool_version_file (__phpenv_find_version_file .tool-version) + if test -n "$phpenv_tool_version_file" + set -l phpenv_version (__phpenv_parse_tool_version $phpenv_tool_version_file) + if test -n "$phpenv_version" + echo $phpenv_version + return + end + end + + if test -f composer.json + set -l phpenv_version (__phpenv_parse_composer_version) + if test -n "$phpenv_version" + echo $phpenv_version + return + end + end + + if test -n "$PHPENV_GLOBAL_VERSION" + echo $PHPENV_GLOBAL_VERSION + return + end + + set -l phpenv_global_version (__phpenv_config_get global-version) + if test -n "$phpenv_global_version" + echo $phpenv_global_version + return + end + + if command -q php + php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;" 2>/dev/null + end +end + +function __phpenv_find_version_file -a phpenv_filename + set -l phpenv_dir (pwd) + while test "$phpenv_dir" != "/" + if test -f "$phpenv_dir/$phpenv_filename" + echo "$phpenv_dir/$phpenv_filename" + return + end + set phpenv_dir (dirname $phpenv_dir) + end +end + +function __phpenv_parse_tool_version -a phpenv_file + if test -f $phpenv_file + set -l phpenv_line (grep "^php " $phpenv_file) + if test -n "$phpenv_line" + set -l phpenv_parts (string split ' ' $phpenv_line) + if test (count $phpenv_parts) -ge 2 + echo $phpenv_parts[2] | sed 's/^v//' + end + end + end +end + +function __phpenv_parse_composer_version + if not test -f composer.json + return + end + + set -l phpenv_platform_php (jq -r '.config.platform.php // empty' composer.json 2>/dev/null) + if test $status -eq 0 -a -n "$phpenv_platform_php" -a "$phpenv_platform_php" != "null" + echo $phpenv_platform_php + return + end + + set -l phpenv_require_php (jq -r '.require.php // empty' composer.json 2>/dev/null) + if test $status -eq 0 -a -n "$phpenv_require_php" -a "$phpenv_require_php" != "null" + __phpenv_parse_semver_constraint $phpenv_require_php + return + end +end + +function __phpenv_parse_semver_constraint -a phpenv_constraint + set phpenv_constraint (echo $phpenv_constraint | tr -d ' "') + + set -l phpenv_latest_8x (__phpenv_parse_version_field "8.x" "8.4") + set -l phpenv_latest_7x (__phpenv_parse_version_field "7.x" "7.4") + set -l phpenv_latest (__phpenv_parse_version_field "latest" "8.4") + + switch $phpenv_constraint + case '^8.*' + echo $phpenv_latest_8x + case '^7.*' + echo $phpenv_latest_7x + case '~8.4*' + echo "8.4" + case '~8.3*' + echo "8.3" + case '~8.2*' + echo "8.2" + case '~8.1*' + echo "8.1" + case '~8.0*' + echo "8.0" + case '~7.4*' + echo "7.4" + case '>=8.1' + echo $phpenv_latest_8x + case '>=8.0' + echo $phpenv_latest_8x + case '>=7.4' + echo $phpenv_latest_8x + case '8.*' '8.x.*' + echo $phpenv_latest_8x + case '7.*' '7.x.*' + echo $phpenv_latest_7x + case '5.*' '5.x.*' + echo "5.6" + case '*' + if echo $phpenv_constraint | grep -q '[0-9]\+\.[0-9]\+' + echo $phpenv_constraint | sed 's/[^0-9\.]//g' | cut -d. -f1,2 + else + echo $phpenv_latest + end + end +end + +# Cache version info to avoid repeated API calls +set -g __phpenv_version_cache +set -g __phpenv_version_cache_time 0 + +function __phpenv_get_version_info + set -l current_time (date +%s) + set -l cache_duration 300 # 5 minutes + + # Return cached version if still valid + if test -n "$__phpenv_version_cache" + set -l cache_age (math $current_time - $__phpenv_version_cache_time) + if test $cache_age -lt $cache_duration + echo $__phpenv_version_cache + return + end + end + + if command -q curl + set -l url https://raw.githubusercontent.com/shivammathur/setup-php/refs/heads/main + set -l version_data (curl -s "$url/src/configs/php-versions.json" 2>/dev/null) + if test -n "$version_data" + set -g __phpenv_version_cache $version_data + set -g __phpenv_version_cache_time $current_time + echo $version_data + end + end +end + +# Cache cellar path as it doesn't change +set -g __phpenv_cellar_cache + +function __phpenv_get_cellar_path + if test -n "$__phpenv_cellar_cache" + echo $__phpenv_cellar_cache + return + end + + if test -d /opt/homebrew/Cellar + set -g __phpenv_cellar_cache /opt/homebrew/Cellar + else if test -d /usr/local/Cellar + set -g __phpenv_cellar_cache /usr/local/Cellar + else + set -g __phpenv_cellar_cache "" + end + + echo $__phpenv_cellar_cache +end + +function __phpenv_ensure_taps + if not command -q brew + return 1 + end + + # Add taps if not already added + if not brew tap | grep -q shivammathur/php 2>/dev/null + brew tap shivammathur/php 2>/dev/null + end + if not brew tap | grep -q shivammathur/extensions 2>/dev/null + brew tap shivammathur/extensions 2>/dev/null + end +end + +function __phpenv_parse_version_field -a field fallback + set -l version_info (__phpenv_get_version_info) + if test -n "$version_info" + echo $version_info | jq -r ".$field // \"$fallback\"" 2>/dev/null + else + echo $fallback + end +end + +function __phpenv_list_installed + set -l phpenv_versions + set -l phpenv_cellar_path (__phpenv_get_cellar_path) + + if test -d $phpenv_cellar_path + for phpenv_dir in $phpenv_cellar_path/php@* $phpenv_cellar_path/php + if test -d $phpenv_dir + set -l phpenv_basename (basename $phpenv_dir) + + if echo $phpenv_basename | grep -qE '(debug|zts)' + continue + end + + if test "$phpenv_basename" = "php" + set -l phpenv_latest (__phpenv_parse_version_field "latest" "8.4") + set -a phpenv_versions $phpenv_latest + else if echo $phpenv_basename | grep -qE '^php@[0-9]+\.[0-9]+$' + set -l phpenv_version (echo $phpenv_basename | sed 's/php@//') + set -a phpenv_versions $phpenv_version + end + end + end + end + + printf '%s\n' $phpenv_versions | sort -V | uniq +end + +function __phpenv_resolve_version_alias -a phpenv_version + switch $phpenv_version + case latest + __phpenv_parse_version_field "latest" "8.4" + case nightly + __phpenv_parse_version_field "nightly" "8.5" + case '8.x' + __phpenv_parse_version_field "8.x" "8.4" + case '7.x' + __phpenv_parse_version_field "7.x" "7.4" + case '5.x' + __phpenv_parse_version_field "5.x" "5.6" + case '*' + echo $phpenv_version + end +end + +function __phpenv_get_formula_name -a phpenv_version + set -l phpenv_latest_version (__phpenv_parse_version_field "latest" "8.4") + + if test "$phpenv_version" = "$phpenv_latest_version" + echo "shivammathur/php/php" + else + echo "shivammathur/php/php@$phpenv_version" + end +end + +function __phpenv_is_version_installed -a phpenv_version + set -l phpenv_cellar_path (__phpenv_get_cellar_path) + set -l phpenv_latest_version (__phpenv_parse_version_field "latest" "8.4") + + if test "$phpenv_version" = "$phpenv_latest_version" + test -d "$phpenv_cellar_path/php" -o -d "$phpenv_cellar_path/php@$phpenv_version" + else + test -d "$phpenv_cellar_path/php@$phpenv_version" + end +end + +function __phpenv_get_php_path -a phpenv_version + set -l phpenv_cellar_path (__phpenv_get_cellar_path) + set -l phpenv_latest_version (__phpenv_parse_version_field "latest" "8.4") + + set -l phpenv_target_dir + if test "$phpenv_version" = "$phpenv_latest_version" + if test -d "$phpenv_cellar_path/php" + set phpenv_target_dir "$phpenv_cellar_path/php" + else if test -d "$phpenv_cellar_path/php@$phpenv_version" + set phpenv_target_dir "$phpenv_cellar_path/php@$phpenv_version" + end + else + if test -d "$phpenv_cellar_path/php@$phpenv_version" + set phpenv_target_dir "$phpenv_cellar_path/php@$phpenv_version" + end + end + + if test -n "$phpenv_target_dir" + # Find the latest version directory by sorting + set -l phpenv_versions + for phpenv_dir in $phpenv_target_dir/* + if test -d "$phpenv_dir" + set -a phpenv_versions (basename "$phpenv_dir") + end + end + + if test (count $phpenv_versions) -gt 0 + set -l phpenv_latest_dir (printf '%s\n' $phpenv_versions | sort -V | tail -1) + if test -n "$phpenv_latest_dir" + echo "$phpenv_target_dir/$phpenv_latest_dir" + end + end + end +end + +function __phpenv_set_php_path -a phpenv_version + set -l phpenv_php_path (__phpenv_get_php_path $phpenv_version) + if test -z "$phpenv_php_path" + echo "Failed to locate PHP $phpenv_version installation path" >&2 + return 1 + end + + if not test -x "$phpenv_php_path/bin/php" + echo "PHP binary not found at $phpenv_php_path/bin/php" >&2 + return 1 + end + + # Store original PATH if not already stored + if not set -q PHPENV_ORIGINAL_PATH + set -g PHPENV_ORIGINAL_PATH $PATH + end + + # Check if we're already using this version + if set -q PHPENV_CURRENT_VERSION; and test "$PHPENV_CURRENT_VERSION" = "$phpenv_version" + return 0 + end + + # Build clean PATH without any PHP paths + set -l phpenv_clean_path + for phpenv_path_entry in $PHPENV_ORIGINAL_PATH + if not echo $phpenv_path_entry | grep -qE "/(Cellar|opt/homebrew)/(php|php@)" + set -a phpenv_clean_path $phpenv_path_entry + end + end + + # Set new PATH with PHP version at front + set -gx PATH "$phpenv_php_path/bin" $phpenv_clean_path + set -g PHPENV_CURRENT_VERSION $phpenv_version + set -g PHPENV_CURRENT_PATH "$phpenv_php_path/bin" + + # Verify the change worked + if command -q php + set -l phpenv_active_version (php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;" 2>/dev/null) + if test "$phpenv_active_version" != "$phpenv_version" + echo "Warning: PHP $phpenv_version set in PATH but php command shows $phpenv_active_version" >&2 + echo "PATH may have conflicting PHP installations" >&2 + end + end +end + +function __phpenv_restore_system_path + if set -q PHPENV_ORIGINAL_PATH + set -gx PATH $PHPENV_ORIGINAL_PATH + set -e PHPENV_CURRENT_VERSION + set -e PHPENV_CURRENT_PATH + set -e PHPENV_ORIGINAL_PATH + return 0 + else + echo "No original PATH stored to restore" + return 1 + end +end + +function __phpenv_install -a phpenv_version + if test -z "$phpenv_version" + echo "Usage: phpenv install " + return 1 + end + + set phpenv_version (__phpenv_resolve_version_alias $phpenv_version) + + if __phpenv_is_version_installed $phpenv_version + echo "PHP $phpenv_version is already installed" + return 0 + end + + echo "Installing PHP $phpenv_version..." + + if not __phpenv_ensure_taps + echo "Error: Homebrew is required but not available" + return 1 + end + + set -l phpenv_formula (__phpenv_get_formula_name $phpenv_version) + if test -z "$phpenv_formula" + echo "Unknown PHP version: $phpenv_version" + echo "Run 'phpenv versions' to see available versions" + return 1 + end + + if brew install $phpenv_formula + echo "PHP $phpenv_version installed successfully" + + if __phpenv_config_get auto-install-extensions | grep -q true + __phpenv_install_default_extensions $phpenv_version + end + else + echo "Failed to install PHP $phpenv_version" + return 1 + end +end + +function __phpenv_uninstall -a phpenv_version + if test -z "$phpenv_version" + echo "Usage: phpenv uninstall " + return 1 + end + + if not __phpenv_is_version_installed $phpenv_version + echo "PHP $phpenv_version is not installed" + return 1 + end + + set -l phpenv_formula (__phpenv_get_formula_name $phpenv_version) + if brew uninstall $phpenv_formula + echo "PHP $phpenv_version uninstalled successfully" + else + echo "Failed to uninstall PHP $phpenv_version" + return 1 + end +end + +function __phpenv_use + set -l phpenv_version $argv[1] + + # Handle special case: restore system PHP + if test "$phpenv_version" = "system" + __phpenv_restore_system_path + echo "Restored system PHP" + return 0 + end + + if test -z "$phpenv_version" + set phpenv_version (__phpenv_detect_version) + if test -z "$phpenv_version" + echo "No PHP version found for this project" + echo "Usage: phpenv use " + return 1 + end + echo "Detected PHP $phpenv_version for this project" + end + + if not __phpenv_is_version_installed $phpenv_version + if test "$(__phpenv_config_get auto-install)" = "true" + __phpenv_install $phpenv_version + else + echo "PHP $phpenv_version is not installed. Install with: phpenv install $phpenv_version" + return 1 + end + end + + if __phpenv_set_php_path $phpenv_version + echo "Using PHP $phpenv_version" + else + echo "Failed to switch to PHP $phpenv_version" + return 1 + end +end + +function __phpenv_local -a phpenv_version + if test -z "$phpenv_version" + echo "Usage: phpenv local " + return 1 + end + + echo $phpenv_version > .php-version + echo "Set local PHP version to $phpenv_version" +end + +function __phpenv_global -a phpenv_version + if test -z "$phpenv_version" + echo "Usage: phpenv global " + return 1 + end + + set -U PHPENV_GLOBAL_VERSION $phpenv_version + echo "Set global PHP version to $phpenv_version" +end + +function __phpenv_list + set -l phpenv_versions (__phpenv_list_installed) + set -l phpenv_current (__phpenv_detect_version) + + for phpenv_version in $phpenv_versions + if test "$phpenv_version" = "$phpenv_current" + echo "* $phpenv_version" + else + echo " $phpenv_version" + end + end +end + +function __phpenv_versions + echo "Available versions from shivammathur/homebrew-php:" + + set -l phpenv_tap_versions (__phpenv_get_tap_versions) + if test -n "$phpenv_tap_versions" + echo $phpenv_tap_versions + return + end + + # Use cached version info + set -l phpenv_latest (__phpenv_parse_version_field "latest" "8.4") + set -l phpenv_nightly (__phpenv_parse_version_field "nightly" "8.5") + set -l phpenv_version_8x (__phpenv_parse_version_field "8.x" "8.4") + set -l phpenv_version_7x (__phpenv_parse_version_field "7.x" "7.4") + set -l phpenv_version_5x (__phpenv_parse_version_field "5.x" "5.6") + + echo "Stable versions:" + echo " $phpenv_version_5x (5.x latest) $phpenv_version_7x (7.x latest) $phpenv_latest (latest stable)" + echo " $phpenv_nightly (nightly)" + echo " 5.6 7.0 7.1 7.2 7.3 7.4" + echo " 8.0 8.1 8.2 8.3 8.4 8.5" +end + +function __phpenv_get_tap_versions + if not command -q brew + return + end + + set -l phpenv_formulas (brew tap-info shivammathur/php --json 2>/dev/null | \ + jq -r '.[]|(.formula_names[]?)' 2>/dev/null) + + if test -z "$phpenv_formulas" + return + end + + set -l phpenv_versions + set -l phpenv_version_info (__phpenv_get_version_info) + set -l phpenv_latest_version (echo $phpenv_version_info | jq -r '.latest // "8.4"' 2>/dev/null) + + for phpenv_formula in $phpenv_formulas + set -l phpenv_clean_name (echo $phpenv_formula | sed 's|shivammathur/php/||') + + if echo $phpenv_clean_name | grep -qE '(debug|zts|autoconf|bison)' + continue + end + + if test "$phpenv_clean_name" = "php" + set -a phpenv_versions "$phpenv_latest_version (latest)" + else if echo $phpenv_clean_name | grep -qE '^php@[0-9]+\.[0-9]+$' + set -l phpenv_version (echo $phpenv_clean_name | sed 's/php@//') + set -a phpenv_versions $phpenv_version + end + end + + if test (count $phpenv_versions) -gt 0 + printf '%s\n' $phpenv_versions | sort -V | tr '\n' ' ' | sed 's/ $//' + echo "" + end +end + +function __phpenv_which -a phpenv_binary + set -l phpenv_binary (test -n "$phpenv_binary"; and echo $phpenv_binary; or echo "php") + set -l phpenv_version (__phpenv_detect_version) + + if test -n "$phpenv_version" + set -l phpenv_php_path (__phpenv_get_php_path $phpenv_version) + if test -x "$phpenv_php_path/bin/$phpenv_binary" + echo "$phpenv_php_path/bin/$phpenv_binary" + else + echo "$phpenv_binary not found for PHP $phpenv_version" + return 1 + end + else + which $phpenv_binary + end +end + +function __phpenv_doctor + echo "phpenv doctor" + echo "=============" + + if command -q jq + echo "✓ jq is installed" + else + echo "✗ jq is not installed (required)" + end + + if command -q brew + echo "✓ Homebrew is installed" + else + echo "✗ Homebrew is not installed" + return 1 + end + + # Check taps using unified function + set -l tap_status (__phpenv_ensure_taps 2>/dev/null; echo $status) + if test $tap_status -eq 0 + echo "✓ Required Homebrew taps are available" + else + echo "! Some Homebrew taps may need to be added automatically" + end + + set -l phpenv_versions (__phpenv_list_installed) + if test (count $phpenv_versions) -gt 0 + echo "✓ PHP versions installed: "(string join ", " $phpenv_versions) + else + echo "! No PHP versions installed" + end + + set -l phpenv_current (__phpenv_detect_version) + if test -n "$phpenv_current" + echo "✓ Current PHP version: $phpenv_current" + else + echo "! No PHP version detected" + end +end + +function __phpenv_config -a phpenv_action phpenv_key phpenv_value + switch $phpenv_action + case get + __phpenv_config_get $phpenv_key + case set + __phpenv_config_set $phpenv_key $phpenv_value + case list + __phpenv_config_list + case '*' + echo "Usage: phpenv config {get|set|list} [key] [value]" + return 1 + end +end + +function __phpenv_config_get -a phpenv_key + set -l phpenv_value + set -l phpenv_source + + switch $phpenv_key + case global-version + if test -n "$PHPENV_GLOBAL_VERSION" + set phpenv_value $PHPENV_GLOBAL_VERSION + set phpenv_source "fish universal variable" + end + case auto-install + if test -n "$PHPENV_AUTO_INSTALL" + set phpenv_value $PHPENV_AUTO_INSTALL + set phpenv_source "fish universal variable" + end + case auto-install-extensions + if test -n "$PHPENV_AUTO_INSTALL_EXTENSIONS" + set phpenv_value $PHPENV_AUTO_INSTALL_EXTENSIONS + set phpenv_source "fish universal variable" + end + case auto-switch + if test -n "$PHPENV_AUTO_SWITCH" + set phpenv_value $PHPENV_AUTO_SWITCH + set phpenv_source "fish universal variable" + end + case default-extensions + if test -n "$PHPENV_DEFAULT_EXTENSIONS" + set phpenv_value $PHPENV_DEFAULT_EXTENSIONS + set phpenv_source "fish universal variable" + end + end + + if test -z "$phpenv_value" + for phpenv_config_file in ~/.config/fish/conf.d/phpenv.fish ~/.config/phpenv/config ~/.phpenv.fish + if test -f $phpenv_config_file + set -l phpenv_file_value (grep "^$phpenv_key=" $phpenv_config_file | cut -d= -f2- | head -1) + if test -n "$phpenv_file_value" + set phpenv_value $phpenv_file_value + set phpenv_source $phpenv_config_file + break + end + end + end + end + + if test "$argv[2]" = "--verbose" + if test -n "$phpenv_value" + echo "$phpenv_key = $phpenv_value (from $phpenv_source)" + else + echo "$phpenv_key = (not set)" + end + else + echo $phpenv_value + end +end + +function __phpenv_config_set -a phpenv_key phpenv_value + if test -z "$phpenv_value" + echo "Usage: phpenv config set " + return 1 + end + + switch $phpenv_key + case global-version + if __phpenv_validate_version $phpenv_value + set -U PHPENV_GLOBAL_VERSION $phpenv_value + else + echo "Invalid PHP version: $phpenv_value" + echo "Run 'phpenv versions' to see available versions" + return 1 + end + case auto-install + if __phpenv_validate_boolean $phpenv_value + set -g PHPENV_AUTO_INSTALL $phpenv_value + else + echo "Invalid value for auto-install. Use 'true' or 'false'" + return 1 + end + case auto-install-extensions + if __phpenv_validate_boolean $phpenv_value + set -g PHPENV_AUTO_INSTALL_EXTENSIONS $phpenv_value + else + echo "Invalid value for auto-install-extensions. Use 'true' or 'false'" + return 1 + end + case auto-switch + if __phpenv_validate_boolean $phpenv_value + set -g PHPENV_AUTO_SWITCH $phpenv_value + else + echo "Invalid value for auto-switch. Use 'true' or 'false'" + return 1 + end + case default-extensions + if __phpenv_validate_extensions $phpenv_value + set -g PHPENV_DEFAULT_EXTENSIONS $phpenv_value + else + echo "Warning: Some extensions may not be available for all PHP versions" + set -g PHPENV_DEFAULT_EXTENSIONS $phpenv_value + end + case '*' + echo "Unknown config key: $phpenv_key" + echo "Available keys: global-version, auto-install, auto-install-extensions, auto-switch," + echo " default-extensions" + return 1 + end + echo "Set $phpenv_key = $phpenv_value" +end + +function __phpenv_config_list + echo "Configuration (showing sources):" + __phpenv_config_get global-version --verbose + __phpenv_config_get auto-install --verbose + __phpenv_config_get auto-install-extensions --verbose + __phpenv_config_get auto-switch --verbose + __phpenv_config_get default-extensions --verbose +end + +function __phpenv_extensions -a phpenv_action phpenv_extension + switch $phpenv_action + case install + __phpenv_extensions_install $phpenv_extension + case uninstall remove + __phpenv_extensions_uninstall $phpenv_extension + case list ls + __phpenv_extensions_list + case available + __phpenv_extensions_available + case '*' + echo "Usage: phpenv extensions {install|uninstall|list|available} [extension]" + return 1 + end +end + +function __phpenv_extensions_install -a phpenv_extension + if test -z "$phpenv_extension" + echo "Usage: phpenv extensions install " + return 1 + end + + # Check for version override first (from environment, not global variable) + set -l phpenv_version + if test -n "$PHPENV_VERSION_OVERRIDE" + set phpenv_version $PHPENV_VERSION_OVERRIDE + else + set phpenv_version (__phpenv_detect_version) + end + if test -z "$phpenv_version" + echo "No PHP version detected" + return 1 + end + + if not __phpenv_extension_available $phpenv_extension $phpenv_version + echo "Extension $phpenv_extension may not be available for PHP $phpenv_version" + echo "Attempting installation anyway..." + end + + echo "Installing $phpenv_extension for PHP $phpenv_version..." + + if not __phpenv_ensure_taps + echo "Error: Homebrew is required but not available" + return 1 + end + + set -l phpenv_formula "shivammathur/extensions/$phpenv_extension@$phpenv_version" + if brew install $phpenv_formula + echo "$phpenv_extension@$phpenv_version installed successfully" + else + echo "Failed to install $phpenv_extension@$phpenv_version" + echo "Extension may not be available for PHP $phpenv_version" + echo "Run 'phpenv extensions available' to see available extensions" + return 1 + end +end + +function __phpenv_extensions_uninstall -a phpenv_extension + if test -z "$phpenv_extension" + echo "Usage: phpenv extensions uninstall " + return 1 + end + + set -l phpenv_version (__phpenv_detect_version) + if test -z "$phpenv_version" + echo "No PHP version detected" + return 1 + end + + set -l phpenv_formula "shivammathur/extensions/$phpenv_extension@$phpenv_version" + if brew uninstall $phpenv_formula + echo "$phpenv_extension@$phpenv_version uninstalled successfully" + else + echo "Failed to uninstall $phpenv_extension@$phpenv_version" + return 1 + end +end + +function __phpenv_get_available_extensions + if not command -q brew + return 1 + end + + brew tap-info shivammathur/extensions --json 2>/dev/null | \ + jq -r '.[]|(.formula_names[]?)' 2>/dev/null +end + +function __phpenv_extension_available -a phpenv_extension phpenv_version + set -l phpenv_available_extensions (__phpenv_get_available_extensions) + + if test -z "$phpenv_available_extensions" + return 0 # Assume available if can't check + end + + for phpenv_ext_formula in $phpenv_available_extensions + if test "$phpenv_ext_formula" = "shivammathur/extensions/$phpenv_extension@$phpenv_version" + return 0 + end + end + + return 1 +end + +function __phpenv_extensions_available + set -l phpenv_version (__phpenv_detect_version) + if test -z "$phpenv_version" + set phpenv_version "8.3" + end + + echo "Available extensions for PHP $phpenv_version:" + + set -l phpenv_available_extensions (__phpenv_get_available_extensions) + + if test -n "$phpenv_available_extensions" + set -l phpenv_version_extensions + for phpenv_ext_formula in $phpenv_available_extensions + if echo $phpenv_ext_formula | grep -q "@$phpenv_version\$" + set -l phpenv_ext_name (echo $phpenv_ext_formula | \ + sed "s|shivammathur/extensions/||" | sed "s|@$phpenv_version||") + set -a phpenv_version_extensions $phpenv_ext_name + end + end + + if test (count $phpenv_version_extensions) -gt 0 + printf ' %s\n' $phpenv_version_extensions | sort + else + echo " No extensions found for PHP $phpenv_version" + end + else + echo " Unable to fetch extension list or Homebrew not available" + end +end + +function __phpenv_extensions_list + set -l phpenv_version (__phpenv_detect_version) + if test -z "$phpenv_version" + echo "No PHP version detected" + return 1 + end + + set -l phpenv_cellar_path (__phpenv_get_cellar_path) + echo "Extensions for PHP $phpenv_version:" + + if test -d $phpenv_cellar_path + for phpenv_ext_dir in $phpenv_cellar_path/*@$phpenv_version + if test -d $phpenv_ext_dir + set -l phpenv_ext_name (basename $phpenv_ext_dir | sed "s/@$phpenv_version//") + if test "$phpenv_ext_name" != "php" + echo " $phpenv_ext_name" + end + end + end + end +end + +function __phpenv_install_default_extensions -a phpenv_version + set -l phpenv_extensions (__phpenv_config_get default-extensions) + if test -n "$phpenv_extensions" + echo "Installing default extensions for PHP $phpenv_version..." + set -l phpenv_failed_extensions + + for phpenv_ext in (echo $phpenv_extensions | tr ' ' '\n') + if test -n "$phpenv_ext" + echo "Installing $phpenv_ext..." + # Temporarily switch context using local variable + set -l phpenv_saved_version (__phpenv_detect_version) + set -l PHPENV_VERSION_OVERRIDE $phpenv_version + if not env PHPENV_VERSION_OVERRIDE=$phpenv_version __phpenv_extensions_install $phpenv_ext + set -a phpenv_failed_extensions $phpenv_ext + echo "Warning: Failed to install $phpenv_ext for PHP $phpenv_version" + end + end + end + + if test (count $phpenv_failed_extensions) -gt 0 + echo "Some extensions failed to install: "(string join ", " $phpenv_failed_extensions) + echo "You can install them manually later with: phpenv extensions install " + else + echo "All default extensions installed successfully" + end + end +end + +function __phpenv_auto_switch --on-variable PWD + # Debouncing: skip if we just switched directories recently + set -l phpenv_current_time (date +%s) + if set -q PHPENV_LAST_SWITCH_TIME + set -l phpenv_time_diff (math $phpenv_current_time - $PHPENV_LAST_SWITCH_TIME) + if test $phpenv_time_diff -lt 1 + return 0 + end + end + + set -l phpenv_auto_switch (__phpenv_config_get auto-switch) + if test "$phpenv_auto_switch" = "false" + return 0 + end + + if not functions -q __phpenv_detect_version __phpenv_set_php_path + return 0 + end + + set -l phpenv_new_version (__phpenv_detect_version 2>/dev/null) + if test -z "$phpenv_new_version" + return 0 + end + + # Check if we're already using the correct version + if set -q PHPENV_CURRENT_VERSION; and test "$PHPENV_CURRENT_VERSION" = "$phpenv_new_version" + return 0 + end + + if __phpenv_is_version_installed "$phpenv_new_version" 2>/dev/null + __phpenv_set_php_path "$phpenv_new_version" 2>/dev/null + set -g PHPENV_LAST_SWITCH_TIME $phpenv_current_time + else + set -l phpenv_auto_install (__phpenv_config_get auto-install) + if test "$phpenv_auto_install" = "true" + echo "Auto-installing PHP $phpenv_new_version..." + if phpenv install "$phpenv_new_version" + set -g PHPENV_LAST_SWITCH_TIME $phpenv_current_time + end + end + end +end + +function __phpenv_help + echo "phpenv - PHP Version Manager for Fish Shell" + echo "" + echo "Usage: phpenv [args]" + echo "" + echo "Commands:" + echo " install Install a PHP version" + echo " uninstall Uninstall a PHP version" + echo " use [version|system] Use PHP version for current shell (auto-detects if no version)" + echo " 'system' restores original PATH" + echo " local Set PHP version for current project" + echo " global Set global PHP version" + echo " list List installed PHP versions" + echo " current Show current PHP version" + echo " which [binary] Show path to PHP binary" + echo " versions Show all available versions" + echo " doctor Check phpenv installation" + echo " config Manage configuration" + echo " extensions Manage PHP extensions" + echo " help Show this help" + echo "" + echo "Version sources (in order of priority):" + echo " 1. .php-version file" + echo " 2. .tool-version file" + echo " 3. composer.json" + echo " 4. Global version" + echo " 5. System PHP" + echo "" + echo "Configuration:" + echo " auto-switch: Enable automatic PHP version switching (default: true)" + echo " auto-install: Auto-install missing versions (default: false)" + echo "" + echo "For more information, visit:" + echo " https://github.com/ivuorinen/phpenv.fish" +end + +function __phpenv_validate_boolean -a phpenv_value + test "$phpenv_value" = "true" -o "$phpenv_value" = "false" +end + +function __phpenv_validate_version -a phpenv_version + if echo $phpenv_version | grep -qE '^[0-9]+\.[0-9]+$' + return 0 + end + + switch $phpenv_version + case latest nightly '5.x' '7.x' '8.x' + return 0 + case '*' + return 1 + end +end + +function __phpenv_validate_extensions -a phpenv_extensions_string + if echo $phpenv_extensions_string | grep -qE '^[a-zA-Z0-9_-]+( +[a-zA-Z0-9_-]+)*$' + return 0 + else + return 1 + end +end +