diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..0768f62f --- /dev/null +++ b/.github/workflows/claude-code-review.yml @@ -0,0 +1,28 @@ +name: Claude PR Review +on: + pull_request: {types: [opened, synchronize, reopened, ready_for_review]} +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + steps: + - uses: actions/checkout@v6 + with: {fetch-depth: 0} + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{secrets.CLAUDE_CODE_OAUTH_TOKEN}} + anthropic_api_key: ${{secrets.ANTHROPIC_API_KEY}} + prompt: | + Review PR#${{github.event.pull_request.number}} in ${{github.repository}}. + Review: code quality, bugs, performance, security. + Use CLAUDE.md for style. Comment via `gh pr comment`. + claude_args: | + --agentic + --max-iterations 25 + --mcp-config '{"mcpServers":{"sequential-thinking":{"command":"npx","args":["-y","@modelcontextprotocol/server-sequential-thinking"]}}}' + --allowed-tools "mcp__sequential-thinking__sequentialthinking,mcp__github__get_pull_request_diff,mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(npm install),Bash(npm run build),Bash(npm run lint),Bash(npm run test)" diff --git a/.github/workflows/claude-pr-auto-review.yml b/.github/workflows/claude-pr-auto-review.yml deleted file mode 100644 index 3e5d58b4..00000000 --- a/.github/workflows/claude-pr-auto-review.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Auto review PRs -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] -permissions: - contents: read - pull-requests: write -jobs: - auto-review: - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@v6.0.1 - with: - fetch-depth: 1 - - name: Claude PR review - uses: anthropics/claude-code-action@main - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # Claude will fetch the diff and leave inline comments - direct_prompt: | - Review this pull request’s diff for correctness, readability, testing, performance, and DX. - Prefer specific, actionable suggestions. Use inline comments where relevant. - # GitHub tools permitted during the run: - allowed_tools: >- - mcp__github__get_pull_request_diff, - mcp__github__create_pending_pull_request_review, - mcp__github__add_comment_to_pending_review, - mcp__github__submit_pending_pull_request_review diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..f1e19d05 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,33 @@ +name: Claude Mention +on: + issue_comment: {types: [created]} + pull_request_review_comment: {types: [created]} + pull_request_review: {types: [submitted]} + issues: {types: [opened, assigned]} + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: read + steps: + - uses: actions/checkout@v6 + with: {fetch-depth: 0} + - uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{secrets.CLAUDE_CODE_OAUTH_TOKEN}} + anthropic_api_key: ${{secrets.ANTHROPIC_API_KEY}} + claude_args: | + --agentic + --max-iterations 20 + --mcp-config '{"mcpServers":{"sequential-thinking":{"command":"npx","args":["-y","@modelcontextprotocol/server-sequential-thinking"]}}}' + --allowed-tools "mcp__sequential-thinking__sequentialthinking,Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(npm install),Bash(npm run build),Bash(npm run lint),Bash(npm run test)" diff --git a/.github/workflows/codacy.yml b/.github/workflows/codacy.yml deleted file mode 100644 index 3ac78449..00000000 --- a/.github/workflows/codacy.yml +++ /dev/null @@ -1,61 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow checks out code, performs a Codacy security scan -# and integrates the results with the -# GitHub Advanced Security code scanning feature. For more information on -# the Codacy security scan action usage and parameters, see -# https://github.com/codacy/codacy-analysis-cli-action. -# For more information on Codacy Analysis CLI in general, see -# https://github.com/codacy/codacy-analysis-cli. - -name: Codacy Security Scan - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '44 14 * * 6' - -permissions: - contents: read - -jobs: - codacy-security-scan: - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - name: Codacy Security Scan - runs-on: ubuntu-slim - steps: - # Checkout the repository to the GitHub Actions runner - - name: Checkout code - uses: actions/checkout@v6.0.1 - - # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@562ee3e92b8e92df8b67e0a5ff8aa8e261919c08 - with: - # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository - # You can also omit the token and run the tools that support default configurations - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - # Adjust severity of non-security issues - gh-code-scanning-compat: true - # Force 0 exit code to allow SARIF file generation - # This will handover control about PR rejection to the GitHub side - max-allowed-issues: 2147483647 - - # Upload the SARIF file generated in the previous step - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: results.sarif diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index 34aa3411..00000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Dependency Review Action -# -# This Action will scan dependency manifest files that change as part of a Pull Request, -# surfacing known-vulnerable versions of the packages declared or updated in the PR. -# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable -# packages will be blocked from merging. -# -# Source repository: https://github.com/actions/dependency-review-action -# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency review' -on: - pull_request: - branches: [ "main" ] - -# If using a dependency submission action in this workflow this permission will need to be set to: -# -# permissions: -# contents: write -# -# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api -permissions: - contents: read - # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option - pull-requests: write - -jobs: - dependency-review: - runs-on: ubuntu-slim - steps: - - name: 'Checkout repository' - uses: actions/checkout@v6.0.1 - - name: 'Dependency Review' - uses: actions/dependency-review-action@v4 - # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. - with: - comment-summary-in-pr: always - # fail-on-severity: moderate - # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later - # retry-on-snapshot-warnings: true diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml deleted file mode 100644 index 2b59d490..00000000 --- a/.github/workflows/eslint.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# ESLint is a tool for identifying and reporting on patterns -# found in ECMAScript/JavaScript code. -# More details at https://github.com/eslint/eslint -# and https://eslint.org - -name: ESLint - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '32 2 * * 0' - -jobs: - eslint: - name: Run eslint scanning - runs-on: ubuntu-slim - permissions: - contents: read - security-events: write - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - steps: - - name: Checkout code - uses: actions/checkout@v6.0.1 - - name: Install ESLint - run: | - npm install eslint@8.10.0 - npm install @microsoft/eslint-formatter-sarif@3.1.0 - - name: Run ESLint - env: - SARIF_ESLINT_IGNORE_SUPPRESSED: "true" - run: npx eslint . - --config .eslintrc.js - --ext .js,.jsx,.ts,.tsx - --format @microsoft/eslint-formatter-sarif - --output-file eslint-results.sarif - continue-on-error: true - - name: Upload analysis results to GitHub - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: eslint-results.sarif - wait-for-processing: true diff --git a/.github/workflows/gemini-scheduled-triage.yml b/.github/workflows/gemini-scheduled-triage.yml deleted file mode 100644 index a8dd34a4..00000000 --- a/.github/workflows/gemini-scheduled-triage.yml +++ /dev/null @@ -1,214 +0,0 @@ -name: 'πŸ“‹ Gemini Scheduled Issue Triage' - -on: - schedule: - - cron: '0 * * * *' # Runs every hour - pull_request: - branches: - - 'main' - - 'release/**/*' - paths: - - '.github/workflows/gemini-scheduled-triage.yml' - push: - branches: - - 'main' - - 'release/**/*' - paths: - - '.github/workflows/gemini-scheduled-triage.yml' - workflow_dispatch: - -concurrency: - group: '${{ github.workflow }}' - cancel-in-progress: true - -defaults: - run: - shell: 'bash' - -jobs: - triage: - runs-on: 'ubuntu-latest' - timeout-minutes: 7 - permissions: - contents: 'read' - id-token: 'write' - issues: 'read' - pull-requests: 'read' - outputs: - available_labels: '${{ steps.get_labels.outputs.available_labels }}' - triaged_issues: '${{ env.TRIAGED_ISSUES }}' - steps: - - name: 'Get repository labels' - id: 'get_labels' - uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8.0.0 - with: - # NOTE: we intentionally do not use the minted token. The default - # GITHUB_TOKEN provided by the action has enough permissions to read - # the labels. - script: |- - const labels = []; - for await (const response of github.paginate.iterator(github.rest.issues.listLabelsForRepo, { - owner: context.repo.owner, - repo: context.repo.repo, - per_page: 100, // Maximum per page to reduce API calls - })) { - labels.push(...response.data); - } - - if (!labels || labels.length === 0) { - core.setFailed('There are no issue labels in this repository.') - } - - const labelNames = labels.map(label => label.name).sort(); - core.setOutput('available_labels', labelNames.join(',')); - core.info(`Found ${labelNames.length} labels: ${labelNames.join(', ')}`); - return labelNames; - - - name: 'Find untriaged issues' - id: 'find_issues' - env: - GITHUB_REPOSITORY: '${{ github.repository }}' - GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN || github.token }}' - run: |- - echo 'πŸ” Finding unlabeled issues and issues marked for triage...' - ISSUES="$(gh issue list \ - --state 'open' \ - --search 'no:label label:"status/needs-triage"' \ - --json number,title,body \ - --limit '100' \ - --repo "${GITHUB_REPOSITORY}" - )" - - echo 'πŸ“ Setting output for GitHub Actions...' - echo "issues_to_triage=${ISSUES}" >> "${GITHUB_OUTPUT}" - - ISSUE_COUNT="$(echo "${ISSUES}" | jq 'length')" - echo "βœ… Found ${ISSUE_COUNT} issue(s) to triage! 🎯" - - - name: 'Run Gemini Issue Analysis' - id: 'gemini_issue_analysis' - if: |- - ${{ steps.find_issues.outputs.issues_to_triage != '[]' }} - uses: 'google-github-actions/run-gemini-cli@v0' # ratchet:exclude - env: - GITHUB_TOKEN: '' # Do not pass any auth token here since this runs on untrusted inputs - ISSUES_TO_TRIAGE: '${{ steps.find_issues.outputs.issues_to_triage }}' - REPOSITORY: '${{ github.repository }}' - AVAILABLE_LABELS: '${{ steps.get_labels.outputs.available_labels }}' - with: - gcp_location: '${{ vars.GOOGLE_CLOUD_LOCATION }}' - gcp_project_id: '${{ vars.GOOGLE_CLOUD_PROJECT }}' - gcp_service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}' - gcp_workload_identity_provider: '${{ vars.GCP_WIF_PROVIDER }}' - gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' - gemini_cli_version: '${{ vars.GEMINI_CLI_VERSION }}' - gemini_debug: '${{ fromJSON(vars.GEMINI_DEBUG || vars.ACTIONS_STEP_DEBUG || false) }}' - gemini_model: '${{ vars.GEMINI_MODEL }}' - google_api_key: '${{ secrets.GOOGLE_API_KEY }}' - use_gemini_code_assist: '${{ vars.GOOGLE_GENAI_USE_GCA }}' - use_vertex_ai: '${{ vars.GOOGLE_GENAI_USE_VERTEXAI }}' - upload_artifacts: '${{ vars.UPLOAD_ARTIFACTS }}' - workflow_name: 'gemini-scheduled-triage' - settings: |- - { - "model": { - "maxSessionTurns": 25 - }, - "telemetry": { - "enabled": true, - "target": "local", - "outfile": ".gemini/telemetry.log" - }, - "tools": { - "core": [ - "run_shell_command(echo)", - "run_shell_command(jq)", - "run_shell_command(printenv)" - ] - } - } - prompt: '/gemini-scheduled-triage' - - label: - runs-on: 'ubuntu-latest' - needs: - - 'triage' - if: |- - needs.triage.outputs.available_labels != '' && - needs.triage.outputs.available_labels != '[]' && - needs.triage.outputs.triaged_issues != '' && - needs.triage.outputs.triaged_issues != '[]' - permissions: - contents: 'read' - issues: 'write' - pull-requests: 'write' - steps: - - name: 'Mint identity token' - id: 'mint_identity_token' - if: |- - ${{ vars.APP_ID }} - uses: 'actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf' # ratchet:actions/create-github-app-token@v2 - with: - app-id: '${{ vars.APP_ID }}' - private-key: '${{ secrets.APP_PRIVATE_KEY }}' - permission-contents: 'read' - permission-issues: 'write' - permission-pull-requests: 'write' - - - name: 'Apply labels' - env: - AVAILABLE_LABELS: '${{ needs.triage.outputs.available_labels }}' - TRIAGED_ISSUES: '${{ needs.triage.outputs.triaged_issues }}' - uses: 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd' # ratchet:actions/github-script@v8.0.0 - with: - # Use the provided token so that the "gemini-cli" is the actor in the - # log for what changed the labels. - github-token: '${{ steps.mint_identity_token.outputs.token || secrets.GITHUB_TOKEN || github.token }}' - script: |- - // Parse the available labels - const availableLabels = (process.env.AVAILABLE_LABELS || '').split(',') - .map((label) => label.trim()) - .sort() - - // Parse out the triaged issues - const triagedIssues = (JSON.parse(process.env.TRIAGED_ISSUES || '{}')) - .sort((a, b) => a.issue_number - b.issue_number) - - core.debug(`Triaged issues: ${JSON.stringify(triagedIssues)}`); - - // Iterate over each label - for (const issue of triagedIssues) { - if (!issue) { - core.debug(`Skipping empty issue: ${JSON.stringify(issue)}`); - continue; - } - - const issueNumber = issue.issue_number; - if (!issueNumber) { - core.debug(`Skipping issue with no data: ${JSON.stringify(issue)}`); - continue; - } - - // Extract and reject invalid labels - we do this just in case - // someone was able to prompt inject malicious labels. - let labelsToSet = (issue.labels_to_set || []) - .map((label) => label.trim()) - .filter((label) => availableLabels.includes(label)) - .sort() - - core.debug(`Identified labels to set: ${JSON.stringify(labelsToSet)}`); - - if (labelsToSet.length === 0) { - core.info(`Skipping issue #${issueNumber} - no labels to set.`) - continue; - } - - core.debug(`Setting labels on issue #${issueNumber} to ${labelsToSet.join(', ')} (${issue.explanation || 'no explanation'})`) - - await github.rest.issues.setLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: labelsToSet, - }); - } diff --git a/.github/workflows/pyre.yml b/.github/workflows/pyre.yml deleted file mode 100644 index 34aecf3d..00000000 --- a/.github/workflows/pyre.yml +++ /dev/null @@ -1,46 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow integrates Pyre with GitHub's -# Code Scanning feature. -# -# Pyre is a performant type checker for Python compliant with -# PEP 484. Pyre can analyze codebases with millions of lines -# of code incrementally – providing instantaneous feedback -# to developers as they write code. -# -# See https://pyre-check.org - -name: Pyre - -on: - workflow_dispatch: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - pyre: - permissions: - actions: read - contents: read - security-events: write - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@v6.0.1 - with: - submodules: true - - - name: Run Pyre - uses: facebook/pyre-action@12b8d923443ea66cb657facc2e5faac1c8c86e64 - with: - # To customize these inputs: - # See https://github.com/facebook/pyre-action#inputs - repo-directory: './' - requirements-path: 'requirements.txt' diff --git a/.github/workflows/summary.yml b/.github/workflows/summary.yml deleted file mode 100644 index 11c83be5..00000000 --- a/.github/workflows/summary.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Summarize new issues - -on: - issues: - types: [opened] - -jobs: - summary: - runs-on: ubuntu-slim - permissions: - issues: write - models: read - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v6.0.1 - - - name: Run AI inference - id: inference - uses: actions/ai-inference@v2 - with: - prompt: | - Summarize the following GitHub issue in one paragraph: - Title: ${{ github.event.issue.title }} - Body: ${{ github.event.issue.body }} - - - name: Comment with AI summary - run: | - gh issue comment $ISSUE_NUMBER --body '${{ steps.inference.outputs.response }}' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - RESPONSE: ${{ steps.inference.outputs.response }} diff --git a/Home/.local/bin/shopt.sh b/Home/.local/bin/shopt.sh index 614dd74f..45f0ad81 100755 --- a/Home/.local/bin/shopt.sh +++ b/Home/.local/bin/shopt.sh @@ -1,30 +1,35 @@ #!/usr/bin/env bash # shellcheck enable=all shell=bash source-path=SCRIPTDIR set -euo pipefail; shopt -s nullglob globstar -IFS=$'\n\t' ; export LC_ALL=C -readonly RED=$'\e[31m' GRN=$'\e[32m' YLW=$'\e[33m' DEF=$'\e[0m' +IFS=$'\n\t'; export LC_ALL=C + has(){ command -v -- "$1" &>/dev/null; } -die(){ printf '%b%s%b\n' "$RED" "$*" "$DEF" >&2; exit 1; } -log(){ printf '%b%s%b\n' "$GRN" "$*" "$DEF"; } -warn(){ printf '%b%s%b\n' "$YLW" "$*" "$DEF"; } +msg(){ printf '%s\n' "$@"; } +log(){ printf '%s\n' "$@" >&2; } +die(){ printf '%s\n' "$1" >&2; exit "${2:-1}"; } +readonly RED=$'\e[31m' GRN=$'\e[32m' YLW=$'\e[33m' DEF=$'\e[0m' +clog(){ printf '%b%s%b\n' "$GRN" "$*" "$DEF" >&2; } +cwarn(){ printf '%b%s%b\n' "$YLW" "$*" "$DEF" >&2; } +cerr(){ printf '%b%s%b\n' "$RED" "$*" "$DEF" >&2; } usage(){ cat <<'EOF' -Usage: shopt [-rfmscCvh] [-o FILE] [-p PERM] [-e EXT] [-V VARIANT] -Mode: -c,--compile (concat+preprocess) -C,--concat (concat only) +Usage: shopt [-rfmscCh] [-o FILE] [-p PERM] [-e EXT] [-V VARIANT] +Mode: -c,--compile (concat+preprocess) -C,--concat (concat only) Processing: -r,--recursive -f,--format -m,--minify -s,--strip -v,--variants -Output: -o,--output -p,--permission -F,--force -d,--debug -Filter: -e,--extensions -w,--whitelist -x,--regex +Output: -o,--output -p,--permission -F,--force -d,--debug +Filter: -e,--extensions -w,--whitelist -x,--regex Examples: shopt script.sh shopt -m script.sh - shopt -c -o build/app src/ + shopt -c -o build/app -V bash,zsh src/ EOF exit 0 } declare -a files variants=() extensions=() whitelist=() -recursive=0 format=1 minify=0 strip=0 force=0 compile=0 concat=0 debug=0 output="" perm="u+x" regex="" +recursive=0 format=1 minify=0 strip=0 force=0 compile=0 concat=0 debug=0 +output="" perm="u+x" regex="" [[ $# -eq 0 ]] && usage while [[ $# -gt 0 ]]; do @@ -41,6 +46,7 @@ while [[ $# -gt 0 ]]; do -e|--extensions) IFS=',' read -ra extensions <<<"$2"; shift;; -w|--whitelist) IFS=',' read -ra whitelist <<<"$2"; shift;; -x|--regex) regex="$2"; shift;; + -V|--variants) IFS=',' read -ra variants <<<"$2"; shift;; -F|--force) force=1;; -h|--help) usage;; -*) die "Unknown option: $1";; @@ -49,34 +55,27 @@ while [[ $# -gt 0 ]]; do shift done -target="${1:? No file/dir specified}" +target="${1:?No file/dir specified}" [[ ${#extensions[@]} -eq 0 ]] && extensions=(sh bash) -[[ ${#variants[@]} -eq 0 ]] && variants=(bash zsh) +[[ ${#variants[@]} -eq 0 ]] && variants=(bash) for cmd in shfmt shellharden shellcheck awk; do - has "$cmd" || die "Missing: $cmd" + has "$cmd" || die "Missing: $cmd" done - -readonly HAS_SD=$(has sd && echo 1 || echo 0) -readonly HAS_PREPROCESS=$(has preprocess && echo 1 || echo 0) -readonly HAS_BEAUTYSH=$(has beautysh && echo 1 || echo 0) - -((compile && ! HAS_PREPROCESS)) && die "Compile mode needs 'preprocess' (pip install preprocess)" - if ((compile || concat)); then - [[ ! -d $target ]] && die "Compile/concat mode needs directory" + [[ ! -d $target ]] && die "Compile/concat mode needs directory" files=() elif [[ -d $target ]]; then ((recursive == 0)) && die "Use -r for directories" - mapfile -d '' files < <(find "$target" -type f -name '*.sh' -o -name '*.bash' -print0) + mapfile -d '' files < <(find "$target" -type f \( -name '*.sh' -o -name '*.bash' \) -print0) else files=("$target") fi +((${#files[@]} == 0 && !compile && !concat)) && { clog "No scripts found"; exit 0; } +[[ ${#files[@]} -gt 1 && -n $output && $output != - ]] && ((! compile && !concat)) && die "Multiple files require -c/--compile" -((${#files[@]} == 0 && !compile && !concat)) && { log "No scripts found"; exit 0; } -[[ ${#files[@]} -gt 1 && -n $output && $output != - ]] && ((! compile && !concat)) && die "Multiple files with single output unsupported (use -c/--compile)" - -read -r -d '' AWK_STRIP <<'AWK' || : +# AWK script: strip comments/blank lines, keep shebang +read -r -d '' AWK_STRIP <<'AWK' || : NR==1 && /^#!/ { print; next } !/^#/ { hdr=1 } ! hdr { next } @@ -84,28 +83,42 @@ NR==1 && /^#!/ { print; next } { gsub(/[[:space:]]+#.*/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); if(length) print } AWK -prep_preprocess(){ - local in="$1" out="$2" +# Pure bash preprocessor: handles #ifdef SHELL_IS_ +# Syntax: #ifdef VAR / #ifndef VAR / #else / #endif +preprocess_shell(){ + local in="$1" out="$2" def="$3" + local -a stack=() line + local active=1 depth=0 : >"$out" while IFS= read -r line; do - [[ ! $line =~ ^[[:space:]]*#[[:space:]]*if[[:space: ]]+ ]] && printf '%s\n' "$line" >>"$out" + if [[ $line =~ ^[[:space:]]*#ifdef[[:space:]]+(.+)$ ]]; then + local var="${BASH_REMATCH[1]}" + stack+=("$active") + ((depth++)) + ((active == 1 && var == "$def")) && active=1 || active=0 + elif [[ $line =~ ^[[:space:]]*#ifndef[[:space:]]+(.+)$ ]]; then + local var="${BASH_REMATCH[1]}" + stack+=("$active") + ((depth++)) + ((active == 1 && var != "$def")) && active=1 || active=0 + elif [[ $line =~ ^[[:space:]]*#else[[:space:]]*$ ]]; then + ((depth > 0 && ${stack[-1]})) && ((active = ! active)) + elif [[ $line =~ ^[[:space:]]*#endif[[:space:]]*$ ]]; then + ((depth > 0)) && { active="${stack[-1]}"; stack=("${stack[@]:0:${#stack[@]}-1}"); ((depth--)); } + else + ((active == 1)) && printf '%s\n' "$line" >>"$out" + fi done <"$in" } +# Minify: strip comments (except copyright/license in first 10 lines), normalize function syntax minify_enhanced(){ local in="$1" out="$2" : >"$out" while IFS= read -r line; do - [[ $line =~ ^#[[:space:]]*#[[:space: ]]*(define|undef|ifdef|ifndef|if|elif|else|endif|error|include) ]] && { - printf '%s\n' "$line" >>"$out" - continue - } [[ $line =~ ^#! ]] && continue if [[ $line =~ ^[[:space:]]*# ]]; then - ((NR <= 10)) && [[ $line =~ Copyright|License ]] && { - printf '%s\n' "$line" >>"$out" - continue - } + ((NR <= 10)) && [[ $line =~ Copyright|License ]] && { printf '%s\n' "$line" >>"$out"; continue; } continue fi line=$(sed -E 's/^[[:space:]]*function[[:space:]]+([a-zA-Z0-9_]+)[[:space:]]*\{/\1(){/g' <<<"$line") @@ -116,6 +129,7 @@ minify_enhanced(){ sed -i -e '/^:[[:space:]]*'"'"'/,/^'"'"'/d' -e '/^#[[:space:]]*[-─]{5,}/d' "$out" 2>/dev/null || : } +# Concatenate files from base directory, respecting whitelist/exclusions concat_files(){ local base="$1" out="$2" rx="$5" local -n exts="$3" wlist="$4" @@ -132,7 +146,7 @@ concat_files(){ ((found == 0)) && continue fi [[ -n $rx && ! $dirpath =~ $rx ]] && continue - log "Concat: $dname" + clog "Concat: $dname" local -a excl_args=() [[ -f $dirpath/__EXCLUDE_FILES ]] && while IFS= read -r xf; do [[ -n $xf ]] && excl_args+=("!" "-path" "$xf") @@ -141,11 +155,12 @@ concat_files(){ while IFS= read -r -d '' lf; do [[ $lf == *__* || $lf == *_PLACEHOLDER* ]] && continue [[ $lf == *."$ext" ]] && cat "$lf" >>"$out" - done < <(find "$dirpath" -type f "${excl_args[@]}" -print0) + done < <(find "$dirpath" -type f "${excl_args[@]}" -print0 2>/dev/null) done done } +# Optimize single file: normalize, format, lint optimize(){ local f="$1" content out_target="$f" [[ -n $output ]] && { @@ -157,20 +172,24 @@ optimize(){ } content=$(<"$f") ((strip)) && content=$(awk "$AWK_STRIP" <<<"$content") - if ((HAS_SD)); then - content=$(sd '\|\| true' '|| :' <<<"$content") - content=$(sd '\s*\(\)\s*\{' '(){' <<<"$content") - content=$(sd '>\/dev\/null 2>&1' '&>/dev/null' <<<"$content") - else - content=$(sed -e 's/|| true/|| :/g' -e 's/\s*(\)\s*{/(){/g' -e 's/>\/dev\/null 2>&1/\&>\/dev\/null/g' <<<"$content") - fi - ((HAS_BEAUTYSH && format && ! minify)) && content=$(beautysh -i 2 -s paronly --variable-style braces - <<<"$content" 2>/dev/null || echo "$content") + # Normalize patterns with sed (combined for efficiency) + content=$(sed -E ' + s/\|\| true/|| :/g + s/[[:space:]]*\(\)[[:space:]]*\{/(){/g + s/>\/dev\/null[[:space:]]+2>&1/\&>\/dev\/null/g + s/>\/dev\/null[[:space:]]+2>\&1/\&>\/dev\/null/g + ' <<<"$content") + + # Format with shfmt if ((format)); then local -a opts=(-ln bash -bn -i 2 -s) ((minify)) && opts+=(-mn) content=$(shfmt "${opts[@]}" <<<"$content") fi + [[ -z $out_target ]] && { printf '%s' "$content"; return 0; } + + # Apply shellharden + shellcheck local tmp tmp=$(mktemp) trap 'rm -f "$tmp"' RETURN @@ -179,9 +198,10 @@ optimize(){ shellcheck -a -x -s bash -f diff "$tmp" 2>/dev/null | patch -Np1 "$tmp" &>/dev/null || : cat "$tmp" >"$out_target" chmod "$perm" "$out_target" - log "βœ“ $out_target" + clog "βœ“ $out_target" } +# Compile variants: concat β†’ preprocess β†’ minify compile_variants(){ local base="$target" [[ -z $output ]] && die "Compile mode requires -o/--output" @@ -190,40 +210,36 @@ compile_variants(){ local tmp_concat tmp_concat=$(mktemp) trap 'rm -f "$tmp_concat"' RETURN - log "Concatenating files from $base" + clog "Concatenating files from $base" concat_files "$base" "$tmp_concat" extensions whitelist "$regex" for var in "${variants[@]}"; do local VAR="${var^^}" var_lo="${var,,}" - log "Compiling variant: $VAR" - local tmp_prep tmp_proc tmp_mini - tmp_prep=$(mktemp) + clog "Compiling variant: $VAR" + local tmp_proc tmp_mini tmp_proc=$(mktemp) tmp_mini=$(mktemp) - trap 'rm -f "$tmp_prep" "$tmp_proc" "$tmp_mini"' RETURN - prep_preprocess "$tmp_concat" "$tmp_prep" - if ((HAS_PREPROCESS)); then - preprocess -D "SHELL_IS_${VAR}=true" -f -o "$tmp_proc" "$tmp_prep" || die "Preprocessor failed for $VAR" - else - cp "$tmp_prep" "$tmp_proc" - fi + trap 'rm -f "$tmp_proc" "$tmp_mini"' RETURN + preprocess_shell "$tmp_concat" "$tmp_proc" "SHELL_IS_${VAR}" minify_enhanced "$tmp_proc" "$tmp_mini" local final="${out_base}.${var_lo}" mv "$tmp_mini" "$final" chmod "$perm" "$final" - log "βœ“ $final" + clog "βœ“ $final" ((debug)) && { - cp "$tmp_concat" "${final}. debug" - log "Debug: ${final}.debug" + cp "$tmp_concat" "${final}.debug" + clog "Debug: ${final}.debug" } done } + +# Main logic if ((compile)); then compile_variants elif ((concat)); then [[ -z $output ]] && die "Concat mode requires -o/--output" - log "Concatenating files from $target" + clog "Concatenating files from $target" concat_files "$target" "$output" extensions whitelist "$regex" - log "βœ“ $output" + clog "βœ“ $output" else for f in "${files[@]}"; do optimize "$f"