diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 585ad76..3748ab0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,48 +32,48 @@ version: 2 updates: - # GitHub Actions: keep pinned action SHAs / version tags fresh. - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "next" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - commit-message: - prefix: "deps" - # Grouped PRs cut noise: one PR per (security | patch | minor) bundle - # per week instead of N separate PRs. Security PRs still get retargeted - # to `hotfixes` by hotfix-redirector.yml when GHSA refs appear in the - # body โ€” bundling N GHSA fixes into one PR is fine, the redirector - # only needs one match to retarget. - groups: - security: - applies-to: security-updates - patterns: ["*"] - patch: - applies-to: version-updates - update-types: ["patch"] - minor: - applies-to: version-updates - update-types: ["minor"] + # GitHub Actions: keep pinned action SHAs / version tags fresh. + - package-ecosystem: "github-actions" + directory: "/" + target-branch: "next" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + # Grouped PRs cut noise: one PR per (security | patch | minor) bundle + # per week instead of N separate PRs. Security PRs still get retargeted + # to `hotfixes` by hotfix-redirector.yml when GHSA refs appear in the + # body โ€” bundling N GHSA fixes into one PR is fine, the redirector + # only needs one match to retarget. + groups: + security: + applies-to: security-updates + patterns: ["*"] + patch: + applies-to: version-updates + update-types: ["patch"] + minor: + applies-to: version-updates + update-types: ["minor"] - # NPM: package.json + package-lock.json updates. - # Delete this block if your repo isn't a Node project. - - package-ecosystem: "npm" - directory: "/" - target-branch: "next" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - commit-message: - prefix: "deps" - groups: - security: - applies-to: security-updates - patterns: ["*"] - patch: - applies-to: version-updates - update-types: ["patch"] - minor: - applies-to: version-updates - update-types: ["minor"] + # NPM: package.json + package-lock.json updates. + # Delete this block if your repo isn't a Node project. + - package-ecosystem: "npm" + directory: "/" + target-branch: "next" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "deps" + groups: + security: + applies-to: security-updates + patterns: ["*"] + patch: + applies-to: version-updates + update-types: ["patch"] + minor: + applies-to: version-updates + update-types: ["minor"] diff --git a/.github/workflows/branch-retention.yml b/.github/workflows/branch-retention.yml index 56c31e2..c0d2f22 100644 --- a/.github/workflows/branch-retention.yml +++ b/.github/workflows/branch-retention.yml @@ -21,18 +21,18 @@ name: ๐ŸŒฟ Branch Retention on: - pull_request: - types: [closed] - branches: [master, main, next, hotfixes] + pull_request: + types: [closed] + branches: [master, main, next, hotfixes] permissions: - contents: write - pull-requests: read + contents: write + pull-requests: read jobs: - retain: - if: github.event.pull_request.merged == true - uses: CLDMV/.github/.github/workflows/reusable-branch-retention.yml@v4 - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + retain: + if: github.event.pull_request.merged == true + uses: CLDMV/.github/.github/workflows/reusable-branch-retention.yml@v4 + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index d6a4855..b758ee9 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -35,26 +35,26 @@ name: ๐Ÿ“œ CLA on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review] - issue_comment: - types: [created] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + issue_comment: + types: [created] permissions: - contents: read - pull-requests: write - statuses: write - issues: write + contents: read + pull-requests: write + statuses: write + issues: write jobs: - cla: - uses: CLDMV/.github/.github/workflows/reusable-cla.yml@v4 - with: - cla_version: "1.0" - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - CLA_BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_CLA_BOT_APP_CLIENT_ID }} - CLA_BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_CLA_BOT_APP_PRIVATE_KEY }} - TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} - TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + cla: + uses: CLDMV/.github/.github/workflows/reusable-cla.yml@v4 + with: + cla_version: "1.0" + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + CLA_BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_CLA_BOT_APP_CLIENT_ID }} + CLA_BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_CLA_BOT_APP_PRIVATE_KEY }} + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e7f930..1e3685d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,41 +30,41 @@ name: ๐Ÿ” CodeQL on: - push: - branches: [master, main] - # Same fork-PR consideration as ci.yml: pull_request fires for forks; SARIF - # upload to base-repo Security tab fails with read-only token. Acceptable โ€” - # push-to-master analysis after merge catches anything missed. DO NOT use - # pull_request_target (runs base-repo workflow with secrets against fork - # code; dangerous). - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - # Include the v4 integration branches (`next`, `hotfixes`) so feature - # and hotfix PRs trigger CodeQL. Without these, branch protection - # rulesets that require the CodeQL check on `next`/`hotfixes` will - # sit on "waiting for results" indefinitely. Branches that don't - # exist in a given repo simply never trigger the workflow โ€” harmless - # for repos that haven't adopted the v4 staging-branch flow. - branches: [master, main, next, hotfixes] - schedule: - - cron: "37 14 * * 1" # weekly Monday 14:37 UTC; GitHub updates queries over time + push: + branches: [master, main] + # Same fork-PR consideration as ci.yml: pull_request fires for forks; SARIF + # upload to base-repo Security tab fails with read-only token. Acceptable โ€” + # push-to-master analysis after merge catches anything missed. DO NOT use + # pull_request_target (runs base-repo workflow with secrets against fork + # code; dangerous). + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # Include the v4 integration branches (`next`, `hotfixes`) so feature + # and hotfix PRs trigger CodeQL. Without these, branch protection + # rulesets that require the CodeQL check on `next`/`hotfixes` will + # sit on "waiting for results" indefinitely. Branches that don't + # exist in a given repo simply never trigger the workflow โ€” harmless + # for repos that haven't adopted the v4 staging-branch flow. + branches: [master, main, next, hotfixes] + schedule: + - cron: "37 14 * * 1" # weekly Monday 14:37 UTC; GitHub updates queries over time permissions: - security-events: write - contents: read - actions: read + security-events: write + contents: read + actions: read concurrency: - group: codeql-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} jobs: - analyze: - uses: CLDMV/.github/.github/workflows/reusable-codeql.yml@v4 - with: - languages: "javascript-typescript" - # Override defaults if needed: - # queries: "security-extended,security-and-quality" - # paths_ignore: "node_modules/,dist/,coverage/,**/test/**" - # config_file: ".github/codeql-config.yml" - # build_mode: "autobuild" + analyze: + uses: CLDMV/.github/.github/workflows/reusable-codeql.yml@v4 + with: + languages: "javascript-typescript" + # Override defaults if needed: + # queries: "security-extended,security-and-quality" + # paths_ignore: "node_modules/,dist/,coverage/,**/test/**" + # config_file: ".github/codeql-config.yml" + # build_mode: "autobuild" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index b14da3b..6275923 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -33,22 +33,22 @@ name: ๐Ÿค– Dependabot Auto-Merge on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] + pull_request: + types: [opened, reopened, synchronize, ready_for_review] permissions: - contents: write - pull-requests: write + contents: write + pull-requests: write jobs: - automerge: - # Pre-filter at workflow level so this doesn't spin up for every PR. - if: github.event.pull_request.user.login == 'dependabot[bot]' - uses: CLDMV/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v4 - with: - bump_types: "patch,minor" - merge_method: "squash" - # also_for_actors: "renovate[bot]" # extend if you adopt Renovate - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + automerge: + # Pre-filter at workflow level so this doesn't spin up for every PR. + if: github.event.pull_request.user.login == 'dependabot[bot]' + uses: CLDMV/.github/.github/workflows/reusable-dependabot-auto-merge.yml@v4 + with: + bump_types: "patch,minor" + merge_method: "squash" + # also_for_actors: "renovate[bot]" # extend if you adopt Renovate + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c4dac71..9d9de13 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -11,24 +11,24 @@ name: ๐Ÿ”’ Dependency Review on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: [master, main] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [master, main] permissions: - contents: read - pull-requests: write + contents: read + pull-requests: write jobs: - review: - uses: CLDMV/.github/.github/workflows/reusable-dependency-review.yml@v4 - with: - fail_on_severity: "moderate" - # Per-repo license policy override: - # deny_licenses: "AGPL-3.0,LGPL-3.0" # block copyleft for an Apache-2.0 repo - # Bot App credentials. When set, the dependency-review PR comment is - # posted by the consumer's bot App instead of github-actions[bot]. - # Both lines are optional; remove them to fall back to GITHUB_TOKEN. - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + review: + uses: CLDMV/.github/.github/workflows/reusable-dependency-review.yml@v4 + with: + fail_on_severity: "moderate" + # Per-repo license policy override: + # deny_licenses: "AGPL-3.0,LGPL-3.0" # block copyleft for an Apache-2.0 repo + # Bot App credentials. When set, the dependency-review PR comment is + # posted by the consumer's bot App instead of github-actions[bot]. + # Both lines are optional; remove them to fall back to GITHUB_TOKEN. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/feature-pr.yml b/.github/workflows/feature-pr.yml index fcee49e..9ef7b86 100644 --- a/.github/workflows/feature-pr.yml +++ b/.github/workflows/feature-pr.yml @@ -26,34 +26,34 @@ name: ๐Ÿ”€ Feature PR (v4) on: - push: - branches: - # CUSTOMIZE: prune this list to whichever branch prefixes your - # repo uses. Must align with the `case` statement in the reusable. - - 'feat/**' - - 'feature/**' - - 'fix/**' - - 'release/**' - - 'chore/**' - - 'refactor/**' - - 'docs/**' - - 'ci/**' - - 'perf/**' - - 'test/**' - - 'style/**' - - 'hotfix/**' + push: + branches: + # CUSTOMIZE: prune this list to whichever branch prefixes your + # repo uses. Must align with the `case` statement in the reusable. + - "feat/**" + - "feature/**" + - "fix/**" + - "release/**" + - "chore/**" + - "refactor/**" + - "docs/**" + - "ci/**" + - "perf/**" + - "test/**" + - "style/**" + - "hotfix/**" concurrency: - group: feature-pr-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: false + group: feature-pr-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: false jobs: - open-pr: - permissions: - contents: read - pull-requests: write - uses: CLDMV/.github/.github/workflows/workflow-feature-pr.yml@v4 - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + open-pr: + permissions: + contents: read + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-feature-pr.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/hotfix-redirector.yml b/.github/workflows/hotfix-redirector.yml index 830cad6..8a557bb 100644 --- a/.github/workflows/hotfix-redirector.yml +++ b/.github/workflows/hotfix-redirector.yml @@ -32,20 +32,20 @@ name: ๐Ÿ”€ Hotfix PR Redirector (v4) # `opened` only (NOT `edited`): if a maintainer manually re-targets the PR, we # must not fight them by redirecting again. on: - pull_request_target: - types: [opened] + pull_request_target: + types: [opened] concurrency: - group: hotfix-redirector-${{ github.event.pull_request.number }} - cancel-in-progress: true + group: hotfix-redirector-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: - redirect: - permissions: - contents: write - pull-requests: write - uses: CLDMV/.github/.github/workflows/workflow-hotfix-redirector.yml@v4 - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + redirect: + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-hotfix-redirector.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/hotfixes-release.yml b/.github/workflows/hotfixes-release.yml index 9de25c0..16040e4 100644 --- a/.github/workflows/hotfixes-release.yml +++ b/.github/workflows/hotfixes-release.yml @@ -23,33 +23,33 @@ name: ๐Ÿš‘ Hotfixes Release (v4) on: - push: - branches: [hotfixes] - workflow_dispatch: # manual kick โ€” e.g. to open/refresh the PR for content already on `hotfixes` + push: + branches: [hotfixes] + workflow_dispatch: # manual kick โ€” e.g. to open/refresh the PR for content already on `hotfixes` concurrency: - group: hotfixes-release-${{ github.repository }} - cancel-in-progress: false + group: hotfixes-release-${{ github.repository }} + cancel-in-progress: false jobs: - release: - permissions: - contents: write - pull-requests: write - uses: CLDMV/.github/.github/workflows/workflow-hotfixes-release.yml@v4 - with: - package_name: "@cldmv/node-android-tv-remote" - build_command: "echo 'โœ“ no build step'" - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - # Optional release-PR notifier webhooks โ€” each is independently - # opt-in: leave one unset and that channel is silently skipped. - # Delete the lines you don't use. - DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} - DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} - SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} - SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} - GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} - GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} + release: + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-hotfixes-release.yml@v4 + with: + package_name: "@cldmv/node-android-tv-remote" + build_command: "echo 'โœ“ no build step'" + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Optional release-PR notifier webhooks โ€” each is independently + # opt-in: leave one unset and that channel is silently skipped. + # Delete the lines you don't use. + DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} + DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} + SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} + SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} + GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} + GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 05f4152..9f01d65 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -27,18 +27,18 @@ name: ๐Ÿท๏ธ PR Labeler # DO NOT add a checkout step or any step that executes PR-supplied content # (build commands, scripts, test runs, etc.) to this workflow. on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] permissions: - contents: read - pull-requests: write + contents: read + pull-requests: write jobs: - label: - uses: CLDMV/.github/.github/workflows/reusable-pr-labeler.yml@v4 - # Optional. Without these, labels are attributed to github-actions[bot]. - # With these, they're attributed to your CLDMV bot App. - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + label: + uses: CLDMV/.github/.github/workflows/reusable-pr-labeler.yml@v4 + # Optional. Without these, labels are attributed to github-actions[bot]. + # With these, they're attributed to your CLDMV bot App. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/master-commit-audit.yml b/.github/workflows/master-commit-audit.yml index d70847d..1a9bd09 100644 --- a/.github/workflows/master-commit-audit.yml +++ b/.github/workflows/master-commit-audit.yml @@ -21,46 +21,46 @@ name: ๐Ÿงพ Master Commit Audit on: - push: - branches: [master, main] + push: + branches: [master, main] permissions: - contents: read - issues: write + contents: read + issues: write jobs: - audit: - runs-on: ubuntu-latest - steps: - # Optional. Without these, the audit issue is filed by - # github-actions[bot]. With them, the issue is filed by your bot App. - - name: Create App token (falls back to GITHUB_TOKEN) - id: app-token - uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 - with: - client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - env: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + audit: + runs-on: ubuntu-latest + steps: + # Optional. Without these, the audit issue is filed by + # github-actions[bot]. With them, the issue is filed by your bot App. + - name: Create App token (falls back to GITHUB_TOKEN) + id: app-token + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - - name: Audit commit subject - uses: CLDMV/.github/.github/actions/git/jobs/audit-commit-subject@v4 - with: - commit_sha: ${{ github.sha }} - # allowed_patterns omitted -> inherit the canonical default from - # audit-commit-subject (release/chore/merge patterns, including - # the "release: vX.Y.Z - " form). Customize only if - # this repo's conventions genuinely differ โ€” a hardcoded copy - # here goes stale the next time the canonical default changes. - # allowed_patterns: | - # ^release: v\d+\.\d+\.\d+( - .+?)?( \(#\d+\))?$ - # ^chore(\([^)]+\))?: .+ - # ^Merge pull request #\d+ from .+ - # ^feat(\([^)]+\))?: .+ - # Canonical label names from CLDMV/.github's data/github-labels.json - # (note the space after each colon). Replace with names that exist - # in your repo's label catalog. - issue_labels: "type: ci,priority: high" - # issue_assignee: "shinrai" # uncomment to auto-assign - github_token: ${{ steps.app-token.outputs.token }} + - name: Audit commit subject + uses: CLDMV/.github/.github/actions/git/jobs/audit-commit-subject@v4 + with: + commit_sha: ${{ github.sha }} + # allowed_patterns omitted -> inherit the canonical default from + # audit-commit-subject (release/chore/merge patterns, including + # the "release: vX.Y.Z - " form). Customize only if + # this repo's conventions genuinely differ โ€” a hardcoded copy + # here goes stale the next time the canonical default changes. + # allowed_patterns: | + # ^release: v\d+\.\d+\.\d+( - .+?)?( \(#\d+\))?$ + # ^chore(\([^)]+\))?: .+ + # ^Merge pull request #\d+ from .+ + # ^feat(\([^)]+\))?: .+ + # Canonical label names from CLDMV/.github's data/github-labels.json + # (note the space after each colon). Replace with names that exist + # in your repo's label catalog. + issue_labels: "type: ci,priority: high" + # issue_assignee: "shinrai" # uncomment to auto-assign + github_token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/next-release.yml b/.github/workflows/next-release.yml index a5e6e79..a514b52 100644 --- a/.github/workflows/next-release.yml +++ b/.github/workflows/next-release.yml @@ -27,35 +27,35 @@ name: ๐Ÿš€ Next Release (v4) on: - push: - branches: [next] - workflow_dispatch: # manual kick โ€” e.g. to open/refresh the PR for content already on `next` + push: + branches: [next] + workflow_dispatch: # manual kick โ€” e.g. to open/refresh the PR for content already on `next` # Serialize: each run re-resolves the current PR state, so queueing (not # cancelling) avoids a create/refresh race when pushes land back-to-back. concurrency: - group: next-release-${{ github.repository }} - cancel-in-progress: false + group: next-release-${{ github.repository }} + cancel-in-progress: false jobs: - release: - permissions: - contents: write - pull-requests: write - uses: CLDMV/.github/.github/workflows/workflow-next-release.yml@v4 - with: - package_name: "@cldmv/node-android-tv-remote" - build_command: "echo 'โœ“ no build step'" - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - # Optional release-PR notifier webhooks โ€” each is independently - # opt-in: leave one unset and that channel is silently skipped. - # Delete the lines you don't use. - DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} - DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} - SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} - SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} - GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} - GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} + release: + permissions: + contents: write + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-next-release.yml@v4 + with: + package_name: "@cldmv/node-android-tv-remote" + build_command: "echo 'โœ“ no build step'" + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + # Optional release-PR notifier webhooks โ€” each is independently + # opt-in: leave one unset and that channel is silently skipped. + # Delete the lines you don't use. + DISCORD_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PUBLIC_WEBHOOK }} + DISCORD_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_PR_PRIVATE_WEBHOOK }} + SLACK_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PUBLIC_WEBHOOK }} + SLACK_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.SLACK_RELEASE_PR_PRIVATE_WEBHOOK }} + GENERIC_RELEASE_PR_PUBLIC_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PUBLIC_WEBHOOK }} + GENERIC_RELEASE_PR_PRIVATE_WEBHOOK: ${{ secrets.GENERIC_RELEASE_PR_PRIVATE_WEBHOOK }} diff --git a/.github/workflows/next-reset.yml b/.github/workflows/next-reset.yml index 87ba088..6576845 100644 --- a/.github/workflows/next-reset.yml +++ b/.github/workflows/next-reset.yml @@ -25,19 +25,19 @@ name: โ™ป๏ธ Next/Hotfixes Reset (v4) on: - push: - branches: [master, main] + push: + branches: [master, main] concurrency: - group: next-reset-${{ github.repository }} - cancel-in-progress: false + group: next-reset-${{ github.repository }} + cancel-in-progress: false jobs: - sync: - permissions: - contents: write - uses: CLDMV/.github/.github/workflows/workflow-next-reset.yml@v4 - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + sync: + permissions: + contents: write + uses: CLDMV/.github/.github/workflows/workflow-next-reset.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/pr-title-normalizer.yml b/.github/workflows/pr-title-normalizer.yml index adba973..0aa8554 100644 --- a/.github/workflows/pr-title-normalizer.yml +++ b/.github/workflows/pr-title-normalizer.yml @@ -26,20 +26,20 @@ name: ๐Ÿท๏ธ PR Title Normalizer # only (NOT edited): a maintainer hand-editing the title must not kick off a # re-normalize loop. on: - pull_request_target: - types: [opened, synchronize] + pull_request_target: + types: [opened, synchronize] concurrency: - group: pr-title-normalizer-${{ github.event.pull_request.number }} - cancel-in-progress: true + group: pr-title-normalizer-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: - normalize: - permissions: - contents: read - pull-requests: write - uses: CLDMV/.github/.github/workflows/workflow-pr-title-normalizer.yml@v4 - secrets: - # Map your repo/org secrets to the expected names. - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + normalize: + permissions: + contents: read + pull-requests: write + uses: CLDMV/.github/.github/workflows/workflow-pr-title-normalizer.yml@v4 + secrets: + # Map your repo/org secrets to the expected names. + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release-notify.yml b/.github/workflows/release-notify.yml index 8f2b836..dabb880 100644 --- a/.github/workflows/release-notify.yml +++ b/.github/workflows/release-notify.yml @@ -23,15 +23,15 @@ name: ๐Ÿ“ฃ Release Notify on: - release: - types: [published] + release: + types: [published] permissions: - contents: read + contents: read jobs: - notify: - # Defensive: skip untagged releases (mirrors Batch 1.2's filter) - if: github.event.release.tag_name != '' - uses: CLDMV/.github/.github/workflows/reusable-release-notifier.yml@v4 - secrets: inherit + notify: + # Defensive: skip untagged releases (mirrors Batch 1.2's filter) + if: github.event.release.tag_name != '' + uses: CLDMV/.github/.github/workflows/reusable-release-notifier.yml@v4 + secrets: inherit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e7d5327..81a47d5 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -22,12 +22,12 @@ name: ๐Ÿ”ฌ OpenSSF Scorecard on: - branch_protection_rule: - schedule: - - cron: "32 7 * * 1" # weekly Monday 07:32 UTC - push: - branches: [master, main] - workflow_dispatch: + branch_protection_rule: + schedule: + - cron: "32 7 * * 1" # weekly Monday 07:32 UTC + push: + branches: [master, main] + workflow_dispatch: # Caller must grant what the reusable needs โ€” notably id-token: write for the # OpenSSF transparency-log publish. @@ -51,11 +51,11 @@ on: # security-events: write back (job-scoped) if publish_results is set to false # instead. jobs: - analyze: - permissions: - id-token: write - contents: read - actions: read - uses: CLDMV/.github/.github/workflows/reusable-scorecard.yml@v4 - with: - publish_results: true # set false for private repos / to skip the public badge + analyze: + permissions: + id-token: write + contents: read + actions: read + uses: CLDMV/.github/.github/workflows/reusable-scorecard.yml@v4 + with: + publish_results: true # set false for private repos / to skip the public badge diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1470e7f..1afac93 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,29 +18,29 @@ name: ๐Ÿ‚ Stale Issues & PRs on: - schedule: - - cron: "13 5 * * *" # daily 05:13 UTC (off-the-hour to avoid GH cron stampede) - workflow_dispatch: - inputs: - dry_run: - description: "Preview only โ€” no changes will be made" - type: boolean - default: false + schedule: + - cron: "13 5 * * *" # daily 05:13 UTC (off-the-hour to avoid GH cron stampede) + workflow_dispatch: + inputs: + dry_run: + description: "Preview only โ€” no changes will be made" + type: boolean + default: false permissions: - issues: write - pull-requests: write + issues: write + pull-requests: write jobs: - sweep: - uses: CLDMV/.github/.github/workflows/reusable-stale.yml@v4 - with: - dry_run: ${{ github.event.inputs.dry_run == 'true' }} - # Override timers if needed: - # days_before_issue_stale: 60 - # days_before_issue_close: 14 - # days_before_pr_stale: 30 - # days_before_pr_close: 7 - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + sweep: + uses: CLDMV/.github/.github/workflows/reusable-stale.yml@v4 + with: + dry_run: ${{ github.event.inputs.dry_run == 'true' }} + # Override timers if needed: + # days_before_issue_stale: 60 + # days_before_issue_close: 14 + # days_before_pr_stale: 30 + # days_before_pr_close: 7 + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/.github/workflows/tag-health.yml b/.github/workflows/tag-health.yml index f250520..4ee06f4 100644 --- a/.github/workflows/tag-health.yml +++ b/.github/workflows/tag-health.yml @@ -19,46 +19,46 @@ name: ๐Ÿฅ Tag Health on: - schedule: - # Weekly Sunday 04:04 UTC. Off-the-hour to dodge the GitHub :00-cron - # stampede; weekly cadence because tag drift accumulates slowly. - - cron: "4 4 * * 0" - workflow_dispatch: - inputs: - debug: - description: "Enable debug logging for troubleshooting" - type: boolean - required: false - default: false - create_documentation: - description: "Update VERSION_TAGS.md if rolling tags moved" - type: boolean - required: false - default: false - use_gpg: - description: "Enable GPG signing for any tags the sweep creates/recreates" - type: boolean - required: false - default: true + schedule: + # Weekly Sunday 04:04 UTC. Off-the-hour to dodge the GitHub :00-cron + # stampede; weekly cadence because tag drift accumulates slowly. + - cron: "4 4 * * 0" + workflow_dispatch: + inputs: + debug: + description: "Enable debug logging for troubleshooting" + type: boolean + required: false + default: false + create_documentation: + description: "Update VERSION_TAGS.md if rolling tags moved" + type: boolean + required: false + default: false + use_gpg: + description: "Enable GPG signing for any tags the sweep creates/recreates" + type: boolean + required: false + default: true permissions: - contents: write + contents: write jobs: - health: - uses: CLDMV/.github/.github/workflows/reusable-tag-health.yml@v4 - with: - debug: ${{ github.event.inputs.debug == 'true' }} - # Full unified sweep: validates, fixes bot signatures, fixes - # unsigned tags, recovers orphaned releases, relocates orphaned - # tags, and updates rolling major/minor refs. - run_unified_tag_health: true - create_documentation: ${{ github.event.inputs.create_documentation == 'true' }} - use_gpg: ${{ github.event.inputs.use_gpg != 'false' }} - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} - TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} - GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} + health: + uses: CLDMV/.github/.github/workflows/reusable-tag-health.yml@v4 + with: + debug: ${{ github.event.inputs.debug == 'true' }} + # Full unified sweep: validates, fixes bot signatures, fixes + # unsigned tags, recovers orphaned releases, relocates orphaned + # tags, and updates rolling major/minor refs. + run_unified_tag_health: true + create_documentation: ${{ github.event.inputs.create_documentation == 'true' }} + use_gpg: ${{ github.event.inputs.use_gpg != 'false' }} + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }} + TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }} + GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }} diff --git a/.github/workflows/v4-bootstrap.yml b/.github/workflows/v4-bootstrap.yml index 472bd2b..d46d085 100644 --- a/.github/workflows/v4-bootstrap.yml +++ b/.github/workflows/v4-bootstrap.yml @@ -44,63 +44,63 @@ name: ๐Ÿš€ v4 Bootstrap on: - workflow_dispatch: - inputs: - dry_run: - description: "Dry-run: preview every mutation without firing it. Default `true` โ€” set to `false` to actually apply changes." - type: boolean - required: false - default: true - code_security: - description: "Code Security policy. off = disable. public-only = enable only if this repo is public (free). all = enable (paid on private)." - type: choice - required: false - default: "off" - options: - - "off" - - "public-only" - - "all" - secret_protection: - description: "Secret Protection (scanning + push protection) policy. Same shape as code_security." - type: choice - required: false - default: "off" - options: - - "off" - - "public-only" - - "all" - steps: - description: "Subset of phases to run, comma-separated." - required: false - default: "branches,settings,security,rulesets" + workflow_dispatch: + inputs: + dry_run: + description: "Dry-run: preview every mutation without firing it. Default `true` โ€” set to `false` to actually apply changes." + type: boolean + required: false + default: true + code_security: + description: "Code Security policy. off = disable. public-only = enable only if this repo is public (free). all = enable (paid on private)." + type: choice + required: false + default: "off" + options: + - "off" + - "public-only" + - "all" + secret_protection: + description: "Secret Protection (scanning + push protection) policy. Same shape as code_security." + type: choice + required: false + default: "off" + options: + - "off" + - "public-only" + - "all" + steps: + description: "Subset of phases to run, comma-separated." + required: false + default: "branches,settings,security,rulesets" permissions: - contents: read + contents: read jobs: - bootstrap: - name: "๐Ÿš€ Bootstrap v4 (this repo)" - runs-on: ubuntu-latest - steps: - - name: Create App token - id: app-token - # Full-permission App token โ€” bootstrap needs administration:write - # for security toggles + ruleset import, plus contents:write for - # branch creation. - uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 - with: - client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - env: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + bootstrap: + name: "๐Ÿš€ Bootstrap v4 (this repo)" + runs-on: ubuntu-latest + steps: + - name: Create App token + id: app-token + # Full-permission App token โ€” bootstrap needs administration:write + # for security toggles + ruleset import, plus contents:write for + # branch creation. + uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4 + with: + client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + env: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} - - name: Bootstrap - uses: CLDMV/.github/.github/actions/github/jobs/org-bootstrap-repo@v4 - with: - # target_repo defaults to GITHUB_REPOSITORY (this repo). - github_token: ${{ steps.app-token.outputs.token }} - dry_run: ${{ github.event.inputs.dry_run }} - steps: ${{ github.event.inputs.steps }} - code_security: ${{ github.event.inputs.code_security }} - secret_protection: ${{ github.event.inputs.secret_protection }} + - name: Bootstrap + uses: CLDMV/.github/.github/actions/github/jobs/org-bootstrap-repo@v4 + with: + # target_repo defaults to GITHUB_REPOSITORY (this repo). + github_token: ${{ steps.app-token.outputs.token }} + dry_run: ${{ github.event.inputs.dry_run }} + steps: ${{ github.event.inputs.steps }} + code_security: ${{ github.event.inputs.code_security }} + secret_protection: ${{ github.event.inputs.secret_protection }} diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 5c082a4..d5e8d88 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -19,20 +19,20 @@ name: ๐Ÿ‘‹ Welcome Contributor # - We only call REST APIs to read prior interactions and post a comment # DO NOT add a checkout step or any step that executes PR-supplied content. on: - issues: - types: [opened] - pull_request_target: - types: [opened] + issues: + types: [opened] + pull_request_target: + types: [opened] permissions: - issues: write - pull-requests: write + issues: write + pull-requests: write jobs: - welcome: - uses: CLDMV/.github/.github/workflows/reusable-welcome.yml@v4 - # Optional. Without these, the welcome comment is posted by - # github-actions[bot]. With these, it's posted by your CLDMV bot App. - secrets: - BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} - BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} + welcome: + uses: CLDMV/.github/.github/workflows/reusable-welcome.yml@v4 + # Optional. Without these, the welcome comment is posted by + # github-actions[bot]. With these, it's posted by your CLDMV bot App. + secrets: + BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }} + BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }} diff --git a/README.md b/README.md index 1d8642c..f5ff530 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ npm install android-tv-remote ### Basic Usage -```js +````js ```js // ESM (Node.js with type: "module" in package.json) import createRemote from "android-tv-remote"; @@ -98,8 +98,9 @@ createRemote({ ip: "192.168.1.100" }) .then(remote => remote.press.home()) .then(() => remote.press.up()) .then(() => remote.press.ok()); -``` -``` +```` + +```` ### Event-Driven Usage (Recommended) @@ -133,7 +134,7 @@ remote.on("screencap-complete", (data) => { // Use the remote await remote.press.home(); await remote.screencap({ filepath: './screenshot.png' }); -``` +```` ### Async Initialization @@ -148,11 +149,10 @@ remote.on("log", console.log); await remote.press.play(); // Or use .then() -createRemote({ ip: "192.168.1.100" }) - .then(remote => { - remote.on("log", console.log); - return remote.press.play(); - }); +createRemote({ ip: "192.168.1.100" }).then((remote) => { + remote.on("log", console.log); + return remote.press.play(); +}); ``` ### Screenshot Functionality @@ -171,31 +171,31 @@ const stream = await remote.screencap(); const resizedStream = await remote.screencap({ width: 1280, height: 720 }); // Save screenshot to file (non-blocking) -await remote.screencap({ filepath: './screenshot.png' }); +await remote.screencap({ filepath: "./screenshot.png" }); // Resized screenshot saved to file -await remote.screencap({ - width: 640, - height: 360, - filepath: './thumbnail.png' +await remote.screencap({ + width: 640, + height: 360, + filepath: "./thumbnail.png" }); // Quick thumbnail (default 240px width) const thumbStream = await remote.thumbnail(); // Custom thumbnail dimensions -const customThumb = await remote.thumbnail({ - width: 320, - height: 180, - filepath: './thumb.png' +const customThumb = await remote.thumbnail({ + width: 320, + height: 180, + filepath: "./thumb.png" }); // Access last screenshot data -console.log('Last screenshot available:', !!remote.lastScreencapData); +console.log("Last screenshot available:", !!remote.lastScreencapData); // Listen for screenshot events -remote.on('screencap-complete', (data) => { - console.log(`Screenshot completed in ${data.timing.total}ms`); +remote.on("screencap-complete", (data) => { + console.log(`Screenshot completed in ${data.timing.total}ms`); }); ``` @@ -216,24 +216,24 @@ const isAwake = await remote.ensureAwake(); // Configure optimal settings for remote control await remote.setSettings(); // Set optimal settings -await remote.setSettings('get'); // Get current settings +await remote.setSettings("get"); // Get current settings // Wait for device to finish booting (after reboot) await remote.waitBootComplete(60000); // 60 second timeout // Connection management await remote.connect(); -console.log('Connected:', remote.isConnected); +console.log("Connected:", remote.isConnected); await remote.disconnect(); // Listen for device management events -remote.on('log', (data) => { - if (data.source === 'reboot') { - console.log(`Reboot: ${data.message}`); - } - if (data.source === 'ensureAwake') { - console.log(`Wake: ${data.message}`); - } +remote.on("log", (data) => { + if (data.source === "reboot") { + console.log(`Reboot: ${data.message}`); + } + if (data.source === "ensureAwake") { + console.log(`Wake: ${data.message}`); + } }); ``` @@ -251,35 +251,35 @@ await remote.keyboard.text("Hello World!"); await remote.keyboard.text("user@example.com"); // Individual character input -await remote.keyboard.key.h(); // Types "h" -await remote.keyboard.key.e(); // Types "e" -await remote.keyboard.key.l(); // Types "l" -await remote.keyboard.key.l(); // Types "l" -await remote.keyboard.key.o(); // Types "o" +await remote.keyboard.key.h(); // Types "h" +await remote.keyboard.key.e(); // Types "e" +await remote.keyboard.key.l(); // Types "l" +await remote.keyboard.key.l(); // Types "l" +await remote.keyboard.key.o(); // Types "o" // Shifted characters (only available for keys that change when shifted) -await remote.keyboard.key.shift.h(); // Types "H" -await remote.keyboard.key.shift.one(); // Types "!" +await remote.keyboard.key.shift.h(); // Types "H" +await remote.keyboard.key.shift.one(); // Types "!" await remote.keyboard.key.shift.semicolon(); // Types ":" // Special characters and symbols -await remote.keyboard.key.space(); // Types " " +await remote.keyboard.key.space(); // Types " " await remote.keyboard.key.exclamation(); // Types "!" -await remote.keyboard.key.at(); // Types "@" -await remote.keyboard.key.hash(); // Types "#" -await remote.keyboard.key.dollar(); // Types "$" +await remote.keyboard.key.at(); // Types "@" +await remote.keyboard.key.hash(); // Types "#" +await remote.keyboard.key.dollar(); // Types "$" // Control keys -await remote.keyboard.key.tab(); // Tab character -await remote.keyboard.key.enter(); // Enter/Return -await remote.keyboard.key.backspace(); // Backspace +await remote.keyboard.key.tab(); // Tab character +await remote.keyboard.key.enter(); // Enter/Return +await remote.keyboard.key.backspace(); // Backspace // Using keycode fallback (when available for regular keys) -await remote.keyboard.key.a.keycode(); // Sends keycode instead of character +await remote.keyboard.key.a.keycode(); // Sends keycode instead of character // Note: Shift variants are only available for keys that actually change -// when shifted (letters, numbers, and some symbols). Special characters -// like @, #, !, etc. don't have shift variants since they're already +// when shifted (letters, numbers, and some symbols). Special characters +// like @, #, !, etc. don't have shift variants since they're already // the shifted form. Shift keys only support text input, not keycodes, // since Android ADB doesn't support sending multiple keycodes simultaneously. ``` diff --git a/dev/tools/debug-chars.mjs b/dev/tools/debug-chars.mjs index db68ae2..ef60c23 100644 --- a/dev/tools/debug-chars.mjs +++ b/dev/tools/debug-chars.mjs @@ -11,31 +11,29 @@ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. */ +import keyboardKeys from "../../src/data/keyboard-keys.json" with { type: "json" }; -import keyboardKeys from '../../src/data/keyboard-keys.json' with { type: 'json' }; - -console.log('๐Ÿ” Debug: Checking for problematic characters'); +console.log("๐Ÿ” Debug: Checking for problematic characters"); // Test which keys might cause JavaScript issues -Object.keys(keyboardKeys).forEach(keyName => { - try { - // Test if the key name can be used as a JavaScript property - const testObj = {}; - testObj[keyName] = 'test'; - - // Test if the character value might cause issues - const char = keyboardKeys[keyName]; - console.log(`โœ… ${keyName}: "${char}" (${char.charCodeAt(0)})`); - - } catch (error) { - console.log(`โŒ ${keyName}: ERROR - ${error.message}`); - } +Object.keys(keyboardKeys).forEach((keyName) => { + try { + // Test if the key name can be used as a JavaScript property + const testObj = {}; + testObj[keyName] = "test"; + + // Test if the character value might cause issues + const char = keyboardKeys[keyName]; + console.log(`โœ… ${keyName}: "${char}" (${char.charCodeAt(0)})`); + } catch (error) { + console.log(`โŒ ${keyName}: ERROR - ${error.message}`); + } }); -console.log('\n๐Ÿ” Characters with escape sequences:'); -Object.keys(keyboardKeys).forEach(keyName => { - const char = keyboardKeys[keyName]; - if (char.includes('\\') || char.charCodeAt(0) < 32) { - console.log(` ${keyName}: "${char}" (charCode: ${char.charCodeAt(0)})`); - } -}); \ No newline at end of file +console.log("\n๐Ÿ” Characters with escape sequences:"); +Object.keys(keyboardKeys).forEach((keyName) => { + const char = keyboardKeys[keyName]; + if (char.includes("\\") || char.charCodeAt(0) < 32) { + console.log(` ${keyName}: "${char}" (charCode: ${char.charCodeAt(0)})`); + } +}); diff --git a/dev/tools/debug-imports.mjs b/dev/tools/debug-imports.mjs index 6c922a9..2c92cbe 100644 --- a/dev/tools/debug-imports.mjs +++ b/dev/tools/debug-imports.mjs @@ -11,22 +11,21 @@ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. */ +import keyboardKeys from "../../src/data/keyboard-keys.json" with { type: "json" }; +import keycodes from "../../src/data/keycodes.json" with { type: "json" }; -import keyboardKeys from '../../src/data/keyboard-keys.json' with { type: 'json' }; -import keycodes from '../../src/data/keycodes.json' with { type: 'json' }; +console.log("๐Ÿ” Debug: Testing keyboard-keys.json import"); +console.log("keyboardKeys loaded:", Object.keys(keyboardKeys).length, "keys"); +console.log("First few keys:", Object.keys(keyboardKeys).slice(0, 10)); +console.log("Special characters present:"); +console.log(" exclamation:", keyboardKeys.exclamation); +console.log(" hash:", keyboardKeys.hash); +console.log(" asterisk:", keyboardKeys.asterisk); +console.log(" tilde:", keyboardKeys.tilde); -console.log('๐Ÿ” Debug: Testing keyboard-keys.json import'); -console.log('keyboardKeys loaded:', Object.keys(keyboardKeys).length, 'keys'); -console.log('First few keys:', Object.keys(keyboardKeys).slice(0, 10)); -console.log('Special characters present:'); -console.log(' exclamation:', keyboardKeys.exclamation); -console.log(' hash:', keyboardKeys.hash); -console.log(' asterisk:', keyboardKeys.asterisk); -console.log(' tilde:', keyboardKeys.tilde); - -console.log('\n๐Ÿ” Debug: Testing keycodes.json import'); -console.log('keycodes loaded:', Object.keys(keycodes).length, 'keys'); -console.log('Keycodes for special chars:'); -console.log(' asterisk keycode:', keycodes.asterisk); -console.log(' at keycode:', keycodes.at); -console.log(' hash keycode:', keycodes.hash); \ No newline at end of file +console.log("\n๐Ÿ” Debug: Testing keycodes.json import"); +console.log("keycodes loaded:", Object.keys(keycodes).length, "keys"); +console.log("Keycodes for special chars:"); +console.log(" asterisk keycode:", keycodes.asterisk); +console.log(" at keycode:", keycodes.at); +console.log(" hash keycode:", keycodes.hash); diff --git a/dev/tools/debug-keyboard-creation.mjs b/dev/tools/debug-keyboard-creation.mjs index 1fb3f64..bee0de2 100644 --- a/dev/tools/debug-keyboard-creation.mjs +++ b/dev/tools/debug-keyboard-creation.mjs @@ -11,48 +11,53 @@ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. */ - -import createRemote from '../../src/lib/android-tv-remote.mjs'; -import keyboardKeys from '../../src/data/keyboard-keys.json' with { type: 'json' }; +import createRemote from "../../src/lib/android-tv-remote.mjs"; +import keyboardKeys from "../../src/data/keyboard-keys.json" with { type: "json" }; async function debugKeyboardCreation() { - console.log('๐Ÿ” Debug: Keyboard Key Generation'); - - console.log('\n๐Ÿ“‹ Raw keyboardKeys from JSON:'); - console.log('Keys count:', Object.keys(keyboardKeys).length); - console.log('Sample keys:', Object.keys(keyboardKeys).slice(0, 15)); - console.log('Special characters:', Object.keys(keyboardKeys).filter(key => !key.match(/^[a-z0-9]$/))); - - console.log('\n๐Ÿ“‹ Creating remote and checking keyboard object:'); - const remote = await createRemote({ - ip: "192.168.1.100", - autoConnect: false - }); - - console.log('remote.keyboard exists:', !!remote.keyboard); - console.log('remote.keyboard.key exists:', !!remote.keyboard.key); - - const allKeyboardKeys = Object.keys(remote.keyboard.key); - console.log('All keyboard.key properties:', allKeyboardKeys.length); - console.log('Properties that are functions:', allKeyboardKeys.filter(key => typeof remote.keyboard.key[key] === 'function').length); - console.log('Properties that are objects:', allKeyboardKeys.filter(key => typeof remote.keyboard.key[key] === 'object').length); - console.log('Properties that are other:', allKeyboardKeys.filter(key => typeof remote.keyboard.key[key] !== 'function' && typeof remote.keyboard.key[key] !== 'object').length); - - console.log('\n๐Ÿ“‹ Function vs non-function properties:'); - allKeyboardKeys.forEach(key => { - const type = typeof remote.keyboard.key[key]; - if (type !== 'function') { - console.log(` ${key}: ${type}`); - } - }); - - console.log('\n๐Ÿ“‹ Testing specific special character keys:'); - const testKeys = ['exclamation', 'hash', 'asterisk', 'leftParen', 'tilde', 'at', 'plus']; - testKeys.forEach(key => { - const exists = key in remote.keyboard.key; - const type = typeof remote.keyboard.key[key]; - console.log(` ${key}: exists=${exists}, type=${type}`); - }); + console.log("๐Ÿ” Debug: Keyboard Key Generation"); + + console.log("\n๐Ÿ“‹ Raw keyboardKeys from JSON:"); + console.log("Keys count:", Object.keys(keyboardKeys).length); + console.log("Sample keys:", Object.keys(keyboardKeys).slice(0, 15)); + console.log( + "Special characters:", + Object.keys(keyboardKeys).filter((key) => !key.match(/^[a-z0-9]$/)) + ); + + console.log("\n๐Ÿ“‹ Creating remote and checking keyboard object:"); + const remote = await createRemote({ + ip: "192.168.1.100", + autoConnect: false + }); + + console.log("remote.keyboard exists:", !!remote.keyboard); + console.log("remote.keyboard.key exists:", !!remote.keyboard.key); + + const allKeyboardKeys = Object.keys(remote.keyboard.key); + console.log("All keyboard.key properties:", allKeyboardKeys.length); + console.log("Properties that are functions:", allKeyboardKeys.filter((key) => typeof remote.keyboard.key[key] === "function").length); + console.log("Properties that are objects:", allKeyboardKeys.filter((key) => typeof remote.keyboard.key[key] === "object").length); + console.log( + "Properties that are other:", + allKeyboardKeys.filter((key) => typeof remote.keyboard.key[key] !== "function" && typeof remote.keyboard.key[key] !== "object").length + ); + + console.log("\n๐Ÿ“‹ Function vs non-function properties:"); + allKeyboardKeys.forEach((key) => { + const type = typeof remote.keyboard.key[key]; + if (type !== "function") { + console.log(` ${key}: ${type}`); + } + }); + + console.log("\n๐Ÿ“‹ Testing specific special character keys:"); + const testKeys = ["exclamation", "hash", "asterisk", "leftParen", "tilde", "at", "plus"]; + testKeys.forEach((key) => { + const exists = key in remote.keyboard.key; + const type = typeof remote.keyboard.key[key]; + console.log(` ${key}: exists=${exists}, type=${type}`); + }); } -debugKeyboardCreation().catch(console.error); \ No newline at end of file +debugKeyboardCreation().catch(console.error); diff --git a/package-lock.json b/package-lock.json index 053c9b8..54bb31e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.2", + "version": "2.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.2", + "version": "2.1.3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -26,7 +26,7 @@ "vitest": "^4.1.10" }, "engines": { - "node": ">=20.3.0" + "node": ">=20.9.0" } }, "node_modules/@babel/helper-string-parser": { @@ -109,9 +109,9 @@ } }, "node_modules/@devicefarmer/adbkit": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.3.8.tgz", - "integrity": "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==", + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@devicefarmer/adbkit/-/adbkit-3.3.9.tgz", + "integrity": "sha512-AYxk5G/b2BPEfubcuxVEZEOpsLf0isqrPNJnnwAlUjZAiLdFg/uvLNRDuBhsC/Ar62X+rUhVlvlL+tKp9HCjfQ==", "license": "Apache-2.0", "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", @@ -1882,9 +1882,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 2bc3813..69dc3d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/node-android-tv-remote", - "version": "2.1.2", + "version": "2.1.3", "description": "A Node.js library for controlling Android TV devices via ADB keycodes and text input.", "type": "module", "exports": { diff --git a/src/lib/android-tv-remote.mjs b/src/lib/android-tv-remote.mjs index 3451a6c..93f5407 100644 --- a/src/lib/android-tv-remote.mjs +++ b/src/lib/android-tv-remote.mjs @@ -13,37 +13,37 @@ /** * Android TV Remote module - Event-driven ADB remote control for Android TV devices. - * + * * @module android-tv-remote - * + * * @description * This module provides an event-driven interface for controlling Android TV devices via ADB. * All operations emit events instead of console logging, allowing for better integration * and programmatic handling of device communication. - * + * * @example * // ESM usage with event handling * import createRemote from "@cldmv/node-android-tv-remote"; - * + * * const remote = createRemote({ ip: "192.168.1.100" }); - * + * * // Listen for log events * remote.on('log', (data) => { * console.log(`[${data.level}] ${data.message}`, data.source); * }); - * + * * // Listen for errors * remote.on('error', (data) => { * console.error(`Error from ${data.source}:`, data.error.message); * }); - * + * * // Use the remote * await remote.press.home(); - * + * * @example * // Static create method * import { createAndroidTVRemote } from "@cldmv/node-android-tv-remote"; - * + * * const remote = await createAndroidTVRemote({ ip: "192.168.1.100" }); * remote.on('log', console.log); * await remote.press.play(); @@ -100,12 +100,12 @@ * @property {Object} keyboard - Keyboard interface for all keys, with text and keycode fallback. * @property {Object} keyboard.key - Contains all key functions. * @property {Object} keyboard.key.shift - Contains all shifted key functions. - * + * * @property {function(string, Function): Remote} on - Add event listener. Returns remote instance for chaining. * @property {function(string, Function): Remote} off - Remove event listener. Returns remote instance for chaining. * @property {function(string, Function): Remote} once - Add one-time event listener. Returns remote instance for chaining. * @property {function(string, ...any): boolean} emit - Emit an event. Returns true if event had listeners. - * + * * @fires Remote#log - Emitted for informational messages, warnings, and debug info. * @fires Remote#error - Emitted when errors occur during ADB operations. * @fires Remote#screencap-start - Emitted when screenshot capture begins. @@ -114,7 +114,7 @@ * @fires Remote#screencap-ready - Emitted when final processed stream is ready. * @fires Remote#screencap-saved - Emitted when screenshot is saved to file. * @fires Remote#screencap-complete - Emitted when entire screenshot operation is complete. - * + * * @example * // Event handling examples * remote.on('log', (data) => { @@ -126,11 +126,11 @@ * console.log(`INFO [${data.source}]: ${data.message}`); * } * }); - * + * * remote.on('error', (data) => { * console.error(`FATAL ERROR from ${data.source}:`); * console.error(data.error.stack || data.error.message); - * + * * // Handle specific error types * if (data.error.message.includes('device unauthorized')) { * console.log('Please authorize ADB on your Android TV device'); @@ -189,7 +189,7 @@ function emitLog(level, message, source = "android-tv-remote", data = null) { ...(data && { data }) }); } - + /** * Emit an error event with structured data. * @private @@ -201,14 +201,12 @@ function emitError(error, source = "android-tv-remote", message = null) { // Filter out libspng/PNG processing errors that occur after disconnection // These are common when background operations try to process data after disconnect const errorMsg = error.message || ""; - if (errorMsg.includes('libspng') || - errorMsg.includes('pngload_buffer') || - errorMsg.includes('read error')) { + if (errorMsg.includes("libspng") || errorMsg.includes("pngload_buffer") || errorMsg.includes("read error")) { // Log as debug instead of error to avoid noise emitLog("debug", `PNG processing error (likely post-disconnect): ${errorMsg}`, source); return; } - + emitter.emit("error", { error, source, @@ -216,7 +214,7 @@ function emitError(error, source = "android-tv-remote", message = null) { timestamp: new Date().toISOString() }); } - + /** * Handles disconnect and connection errors, emits helpful messages. * Also provides onboarding steps for common authentication and connection issues. @@ -235,25 +233,49 @@ function handleDisconnectError(err) { if (err.message && (err.message.includes("device unauthorized") || err.message.includes("failed to authenticate"))) { emitError(err, "handleDisconnectError", "Device unauthorized - authentication required"); - emitLog("error", "Your device is unauthorized or failed to authenticate. Please check your TV and accept the authorization dialog to allow this system to connect via ADB.", "handleDisconnectError"); - emitLog("info", "If you do not see a prompt, try disconnecting and reconnecting the device, or reboot your TV.", "handleDisconnectError"); - emitLog("info", "If the problem persists, remove the device from the list of authorized ADB devices in Developer Options and try again.", "handleDisconnectError"); - emitLog("info", "Tip: In Developer Options on your TV, try toggling 'ADB Debugging' off and then back on. This often resolves authentication issues.", "handleDisconnectError"); + emitLog( + "error", + "Your device is unauthorized or failed to authenticate. Please check your TV and accept the authorization dialog to allow this system to connect via ADB.", + "handleDisconnectError" + ); + emitLog( + "info", + "If you do not see a prompt, try disconnecting and reconnecting the device, or reboot your TV.", + "handleDisconnectError" + ); + emitLog( + "info", + "If the problem persists, remove the device from the list of authorized ADB devices in Developer Options and try again.", + "handleDisconnectError" + ); + emitLog( + "info", + "Tip: In Developer Options on your TV, try toggling 'ADB Debugging' off and then back on. This often resolves authentication issues.", + "handleDisconnectError" + ); } - + if (err.message && (err.message.includes("actively refused") || err.message.includes("No connection could be made"))) { emitError(err, "handleDisconnectError", "Connection refused - ADB not enabled"); - emitLog("error", "The device refused the connection. To enable ADB, follow these steps on your Android TV or Fire TV:", "handleDisconnectError"); + emitLog( + "error", + "The device refused the connection. To enable ADB, follow these steps on your Android TV or Fire TV:", + "handleDisconnectError" + ); emitLog("info", "1. Open Settings > Device Preferences > About (or My Fire TV > About)", "handleDisconnectError"); emitLog("info", "2. Scroll to 'Build' and press OK 7 times to enable Developer Options", "handleDisconnectError"); emitLog("info", "3. Go back to Settings > Device Preferences > Developer Options", "handleDisconnectError"); - emitLog("info", "4. Enable 'Developer Options' if needed, then enable 'ADB Debugging' and 'Apps from Unknown Sources'", "handleDisconnectError"); + emitLog( + "info", + "4. Enable 'Developer Options' if needed, then enable 'ADB Debugging' and 'Apps from Unknown Sources'", + "handleDisconnectError" + ); emitLog("info", "5. Ensure your TV and computer are on the same network", "handleDisconnectError"); emitLog("info", "6. On your computer, run: adb connect :5555", "handleDisconnectError"); emitLog("info", "7. Accept the authorization prompt on your TV", "handleDisconnectError"); emitLog("info", "If you do not see 'Developer Options', repeat step 2 until it appears.", "handleDisconnectError"); } - + return err; } @@ -278,16 +300,32 @@ function getShiftedCharacter(keyName, char) { if (/^[a-z]$/.test(char)) { return char.toUpperCase(); } - + // Handle shifted symbols const shiftMap = { - '1': '!', '2': '@', '3': '#', '4': '$', '5': '%', - '6': '^', '7': '&', '8': '*', '9': '(', '0': ')', - '-': '_', '=': '+', '[': '{', ']': '}', '\\': '|', - ';': ':', "'": '"', ',': '<', '.': '>', '/': '?', - '`': '~' + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + "\\": "|", + ";": ":", + "'": '"', + ",": "<", + ".": ">", + "/": "?", + "`": "~" }; - + return shiftMap[char] || char; } @@ -321,7 +359,7 @@ export default async function createRemote(config) { const device = client.getDevice(host); let connected = false; let backgroundOperations = new Set(); - + /** * Local emitError function that has access to connection state * @private @@ -332,18 +370,16 @@ export default async function createRemote(config) { function localEmitError(error, source = "android-tv-remote", message = null) { // Filter out libspng/PNG processing errors that occur after disconnection const errorMsg = error.message || ""; - if (!connected && (errorMsg.includes('libspng') || - errorMsg.includes('pngload_buffer') || - errorMsg.includes('read error'))) { + if (!connected && (errorMsg.includes("libspng") || errorMsg.includes("pngload_buffer") || errorMsg.includes("read error"))) { // Log as debug instead of error to avoid noise emitLog("debug", `PNG processing error (post-disconnect): ${errorMsg}`, source); return; } - + // Use global emitError for other cases emitError(error, source, message); } - + const autoConnect = config.autoConnect !== false; // default true const autoDisconnect = config.autoDisconnect === true; // default false const disconnectTimeout = typeof config.disconnectTimeout === "number" ? config.disconnectTimeout : 10; @@ -543,23 +579,33 @@ export default async function createRemote(config) { disconnectTimer = null; } stopHeartbeat(); - + // Wait for all background operations to complete before disconnecting if (backgroundOperations.size > 0) { - if (!quiet) emitLog("info", `๐Ÿ”„ [DISCONNECT] Waiting for ${backgroundOperations.size} background operations to complete...`, "disconnect"); + if (!quiet) + emitLog("info", `๐Ÿ”„ [DISCONNECT] Waiting for ${backgroundOperations.size} background operations to complete...`, "disconnect"); const waitStartTime = performance.now(); try { await Promise.allSettled([...backgroundOperations]); const waitEndTime = performance.now(); - if (!quiet) emitLog("info", `โœ… [DISCONNECT] All background operations completed in ${(waitEndTime - waitStartTime).toFixed(2)}ms`, "disconnect"); + if (!quiet) + emitLog( + "info", + `โœ… [DISCONNECT] All background operations completed in ${(waitEndTime - waitStartTime).toFixed(2)}ms`, + "disconnect" + ); } catch (error) { const waitEndTime = performance.now(); - emitLog("warn", `โš ๏ธ [DISCONNECT] Some background operations failed after ${(waitEndTime - waitStartTime).toFixed(2)}ms: ${error.message}`, "disconnect"); + emitLog( + "warn", + `โš ๏ธ [DISCONNECT] Some background operations failed after ${(waitEndTime - waitStartTime).toFixed(2)}ms: ${error.message}`, + "disconnect" + ); } } else { if (!quiet) emitLog("info", `โœ… [DISCONNECT] No background operations to wait for`, "disconnect"); } - + try { await client.disconnect(ip, port); connected = false; @@ -700,7 +746,7 @@ export default async function createRemote(config) { * remote.initPromise.then(() => { ... }).catch((err) => { ... }); */ initPromise, - + /** * Add event listener. * @public @@ -715,7 +761,7 @@ export default async function createRemote(config) { emitter.on(event, listener); return remoteApi; }, - + /** * Remove event listener. * @public @@ -729,7 +775,7 @@ export default async function createRemote(config) { emitter.off(event, listener); return remoteApi; }, - + /** * Add one-time event listener. * @public @@ -743,7 +789,7 @@ export default async function createRemote(config) { emitter.once(event, listener); return remoteApi; }, - + /** * Emit an event. * @public @@ -794,7 +840,7 @@ export default async function createRemote(config) { get isConnected() { return isConnected(); }, - + /** * Returns the last screencap data (PNG stream) captured by screencap() or thumbnail(). * Updated every time a screenshot is taken, regardless of options used. @@ -803,7 +849,7 @@ export default async function createRemote(config) { * @example * await remote.screencap({ filepath: './screenshot.png' }); * console.log('Last screencap stream:', remote.lastScreencapData); - * + * * // Use the cached stream data * if (remote.lastScreencapData) { * remote.lastScreencapData.pipe(someOtherStream); @@ -823,27 +869,25 @@ export default async function createRemote(config) { * await remote.setSettings(); // Set all optimal settings * await remote.setSettings('get'); // Get current settings values */ - async setSettings(mode = 'set') { + async setSettings(mode = "set") { try { - emitLog("info", `=== ${mode === 'set' ? 'Configuring' : 'Retrieving'} Android TV settings ===`, "setSettings"); - + emitLog("info", `=== ${mode === "set" ? "Configuring" : "Retrieving"} Android TV settings ===`, "setSettings"); + await ensureConnected(); - + // Core settings for power management and display const coreSettings = [ { ns: "system", key: "screen_off_timeout", value: 2147483647 }, { ns: "secure", key: "sleep_timeout", value: 0 }, { ns: "global", key: "stay_on_while_plugged_in", value: 3 } ]; - + // Process core settings for (const item of coreSettings) { - const cmd = mode === "set" - ? `settings put ${item.ns} ${item.key} ${item.value}` - : `settings get ${item.ns} ${item.key}`; - + const cmd = mode === "set" ? `settings put ${item.ns} ${item.key} ${item.value}` : `settings get ${item.ns} ${item.key}`; + const result = await device.shell(cmd).then(Adb.util.readAll); - + if (!quiet) { if (mode === "set") { emitLog("info", `Set ${item.ns} ${item.key} to ${item.value}`, "setSettings"); @@ -852,27 +896,26 @@ export default async function createRemote(config) { } } } - + // Additional settings (only applied in set mode) - if (mode === 'set') { + if (mode === "set") { emitLog("info", "Setting additional display and power settings", "setSettings"); - + const additionalSettings = [ - "settings put system screen_brightness_mode 0", // Manual brightness - "settings put system screen_brightness 255", // Max brightness - "svc power stayon true" // Stay on while plugged + "settings put system screen_brightness_mode 0", // Manual brightness + "settings put system screen_brightness 255", // Max brightness + "svc power stayon true" // Stay on while plugged ]; - + for (const cmd of additionalSettings) { if (!quiet) emitLog("info", `Running: ${cmd}`, "setSettings"); await device.shell(cmd); } } - - emitLog("info", `โœ… Android TV settings ${mode === 'set' ? 'configured' : 'retrieved'} successfully`, "setSettings"); - + + emitLog("info", `โœ… Android TV settings ${mode === "set" ? "configured" : "retrieved"} successfully`, "setSettings"); } catch (error) { - localEmitError(error, "setSettings", `Failed to ${mode === 'set' ? 'configure' : 'retrieve'} Android TV settings`); + localEmitError(error, "setSettings", `Failed to ${mode === "set" ? "configure" : "retrieve"} Android TV settings`); throw error; } }, @@ -929,7 +972,7 @@ export default async function createRemote(config) { // Determine what commands need to be sent const commandsToSend = []; - + if (initialPowerState.mIsPowered !== "true") { emitLog("info", `Device not powered (${initialPowerState.mIsPowered}), will send POWER keycode`, "ensureAwake"); commandsToSend.push({ keycode: keycodes.power, reason: "power on device" }); @@ -954,18 +997,18 @@ export default async function createRemote(config) { // Send required commands if (commandsToSend.length > 0) { emitLog("info", `Sending ${commandsToSend.length} keycode(s)...`, "ensureAwake"); - + for (const cmd of commandsToSend) { emitLog("info", `Sending keycode ${cmd.keycode} to ${cmd.reason}`, "ensureAwake"); await inputKeycode(cmd.keycode); - + // Brief delay between commands - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); } // Wait for commands to take effect emitLog("info", "Waiting for commands to take effect...", "ensureAwake"); - await new Promise(resolve => setTimeout(resolve, 3000)); + await new Promise((resolve) => setTimeout(resolve, 3000)); } // Verify final state @@ -992,16 +1035,15 @@ export default async function createRemote(config) { for (const issue of issues) { emitLog("warn", issue, "ensureAwake"); } - + // Only throw error if critical issues persist emitError( - new Error(`ensureAwake failed with ${issues.length} persistent issue(s): ${issues.join(', ')}`), - "ensureAwake", + new Error(`ensureAwake failed with ${issues.length} persistent issue(s): ${issues.join(", ")}`), + "ensureAwake", "Device may not be fully responsive" ); return false; } - } catch (error) { emitError(error, "ensureAwake", "Failed to ensure device is awake"); throw error; @@ -1019,9 +1061,10 @@ export default async function createRemote(config) { * await remote.connect(); * remote.connect((err) => { ... }); */ - connect: /** - * @type {(cb?: (err?: Error, result?: any) => any) => Promise | undefined} - */ (connectWrapped), + connect: + /** + * @type {(cb?: (err?: Error, result?: any) => any) => Promise | undefined} + */ (connectWrapped), /** * Disconnect from the device. Supports both promise and callback styles. @@ -1034,9 +1077,10 @@ export default async function createRemote(config) { * await remote.disconnect(); * remote.disconnect((err) => { ... }); */ - disconnect: /** - * @type {(cb?: (err?: Error, result?: any) => any) => Promise | undefined} - */ (disconnectWrapped), + disconnect: + /** + * @type {(cb?: (err?: Error, result?: any) => any) => Promise | undefined} + */ (disconnectWrapped), /** * Send a keycode. Supports both promise and callback styles. @@ -1048,9 +1092,10 @@ export default async function createRemote(config) { * await remote.inputKeycode(23); * remote.inputKeycode(23, (err) => { ... }); */ - inputKeycode: /** - * @type {(code: number, cb?: (err?: Error, result?: any) => any) => Promise | undefined} - */ (inputKeycodeWrapped), + inputKeycode: + /** + * @type {(code: number, cb?: (err?: Error, result?: any) => any) => Promise | undefined} + */ (inputKeycodeWrapped), /** * Reboots the Android TV device using ADB's native reboot method. @@ -1064,12 +1109,12 @@ export default async function createRemote(config) { async reboot() { try { emitLog("info", "=== Starting device reboot ===", "reboot"); - + // Ensure we're connected before sending reboot command await ensureConnected(); - + emitLog("info", "Sending reboot command to device", "reboot"); - + // Use the native device.reboot() method with timeout handling let result; try { @@ -1086,7 +1131,7 @@ export default async function createRemote(config) { } } emitLog("warn", "Device will reboot shortly and connection will be lost", "reboot"); - + // Wait a moment for command to take effect, then disconnect setTimeout(() => { emitLog("info", "Device should be rebooting now - connection will be terminated", "reboot"); @@ -1097,10 +1142,9 @@ export default async function createRemote(config) { emitLog("info", "=== Device reboot initiated ===", "reboot"); return result; - } catch (error) { emitError(error, "reboot", "Failed to reboot device"); - + // Provide helpful error messages for common issues if (error.message && error.message.includes("unauthorized")) { emitLog("error", "Device unauthorized - ensure ADB debugging is enabled and device is authorized", "reboot"); @@ -1111,7 +1155,7 @@ export default async function createRemote(config) { } else if (error.message && error.message.includes("timeout")) { emitLog("warn", "Reboot command timed out - this may be normal behavior during reboot", "reboot"); } - + throw error; } }, @@ -1121,7 +1165,7 @@ export default async function createRemote(config) { * @public * @param {Object} [options={}] - Screenshot options * @param {number} [options.width] - Target width for resizing (optional) - * @param {number} [options.height] - Target height for resizing (optional) + * @param {number} [options.height] - Target height for resizing (optional) * @param {string} [options.filepath] - File path to save screenshot (optional) * @returns {Promise} Returns PNG stream if no filepath, void if saved to file * @fires Remote#screencap-start - Emitted when screenshot capture begins @@ -1135,16 +1179,16 @@ export default async function createRemote(config) { * @example * // Basic screenshot - returns stream * const pngStream = await remote.screencap(); - * + * * // Screenshot with resizing * const resizedStream = await remote.screencap({ width: 1280, height: 720 }); - * + * * // Screenshot saved to file * await remote.screencap({ filepath: './screenshot.png' }); - * + * * // Screenshot with resizing and file save * await remote.screencap({ width: 640, height: 360, filepath: './thumb.png' }); - * + * * // Event-driven usage (non-blocking) * remote.screencap({ width: 1280, filepath: './shot.png' }); * remote.on('screencap-complete', (data) => { @@ -1154,27 +1198,27 @@ export default async function createRemote(config) { async screencap(options = {}) { const startTime = performance.now(); const { width, height, filepath } = options; - + try { emitLog("info", "=== Taking device screenshot ===", "screencap"); - + // Emit start event emitter.emit("screencap-start", { timestamp: new Date().toISOString(), options: { width, height, filepath } }); - + // Ensure we're connected before taking screenshot await ensureConnected(); - + emitLog("info", "Capturing screenshot using native screencap utility", "screencap"); - + // Use the native device.screencap() method which should return PNG const screencapStream = await device.screencap(); const captureTime = performance.now(); - + emitLog("info", `โœ… Screenshot captured successfully (${(captureTime - startTime).toFixed(2)}ms)`, "screencap"); - + // Emit captured event emitter.emit("screencap-captured", { timestamp: new Date().toISOString(), @@ -1183,44 +1227,44 @@ export default async function createRemote(config) { // Determine if we need Sharp processing (only for resizing) const needsSharpProcessing = width || height; - + // Handle file save without processing (direct PNG stream to file) if (filepath && !needsSharpProcessing) { const backgroundOperation = (async () => { try { const fileOpStartTime = performance.now(); - + emitLog("info", `๐Ÿ’พ [BACKGROUND] Starting direct save to: ${filepath}`, "screencap"); - + // Direct pipe: ADB PNG stream -> File (no Sharp processing) const directPipeStartTime = performance.now(); const writeStream = createWriteStream(filepath); - + // Store the raw PNG stream for user access lastScreencapData = screencapStream; - + emitLog("info", `๐Ÿ”„ [BACKGROUND] Piping stream to file...`, "screencap"); screencapStream.pipe(writeStream); - + emitLog("info", `โณ [BACKGROUND] Waiting for write stream to finish...`, "screencap"); await new Promise((resolve, reject) => { - writeStream.on('finish', () => { + writeStream.on("finish", () => { emitLog("info", `โœ… [BACKGROUND] Write stream finished for ${filepath}`, "screencap"); resolve(); }); - writeStream.on('error', (err) => { + writeStream.on("error", (err) => { emitLog("error", `โŒ [BACKGROUND] Write stream error for ${filepath}: ${err.message}`, "screencap"); reject(err); }); }); - + const directPipeTime = performance.now() - directPipeStartTime; const totalFileOpTime = performance.now() - fileOpStartTime; const totalTime = performance.now() - startTime; - + emitLog("info", `โœ… Screenshot saved directly to ${filepath}`, "screencap"); emitLog("info", `โšก Direct pipe timing: ${directPipeTime.toFixed(2)}ms (no Sharp processing!)`, "screencap"); - + // Emit saved event with timing emitter.emit("screencap-saved", { timestamp: new Date().toISOString(), @@ -1232,7 +1276,7 @@ export default async function createRemote(config) { total: totalTime } }); - + // Emit complete event emitter.emit("screencap-complete", { timestamp: new Date().toISOString(), @@ -1245,7 +1289,6 @@ export default async function createRemote(config) { total: totalTime } }); - } catch (saveError) { emitLog("error", `๐Ÿ’ฅ [BACKGROUND] Direct pipe error: ${saveError.message}`, "screencap"); localEmitError(saveError, "screencap", `Failed to save screenshot directly to ${filepath}: ${saveError.message}`); @@ -1254,16 +1297,16 @@ export default async function createRemote(config) { backgroundOperations.delete(backgroundOperation); } })(); - + backgroundOperations.add(backgroundOperation); emitLog("info", "Direct PNG pipe started in background", "screencap"); return; // Don't return a stream when saving to file } - + if (!needsSharpProcessing && !filepath) { // Store the raw PNG stream for user access lastScreencapData = screencapStream; - + // Return raw stream if no processing or file save needed emitLog("info", "Raw PNG stream ready (no processing needed)", "screencap"); emitter.emit("screencap-ready", { @@ -1278,7 +1321,7 @@ export default async function createRemote(config) { }); return screencapStream; } - + // Process the image emitLog("info", "Processing screenshot image...", "screencap"); emitter.emit("screencap-processing", { @@ -1287,31 +1330,31 @@ export default async function createRemote(config) { height, filepath }); - + const processStartTime = performance.now(); - + // Create Sharp transform pipeline let sharpTransform = sharp(); - + // Apply resizing if width or height specified if (width || height) { const resizeOptions = { - fit: 'inside', // Maintain aspect ratio + fit: "inside", // Maintain aspect ratio withoutEnlargement: true // Don't upscale }; if (width) resizeOptions.width = width; if (height) resizeOptions.height = height; - + sharpTransform = sharpTransform.resize(resizeOptions); - emitLog("info", `Resizing to ${width || 'auto'}x${height || 'auto'}`, "screencap"); + emitLog("info", `Resizing to ${width || "auto"}x${height || "auto"}`, "screencap"); } - + // Ensure PNG format with fastest compression settings - sharpTransform = sharpTransform.png({ + sharpTransform = sharpTransform.png({ compressionLevel: 1, // Fastest compression (0-9, lower = faster) - progressive: false // Disable progressive encoding + progressive: false // Disable progressive encoding }); - + if (filepath) { // Background process handles saving and updates lastScreencapData when done const backgroundOperation = (async () => { @@ -1320,34 +1363,34 @@ export default async function createRemote(config) { const writeStreamStartTime = performance.now(); const writeStream = createWriteStream(filepath); const writeStreamTime = performance.now() - writeStreamStartTime; - + // Process and save data, collecting it for lastScreencapData const chunks = []; const processedStream = screencapStream.pipe(sharpTransform); - + await new Promise((resolve, reject) => { - processedStream.on('data', (chunk) => { + processedStream.on("data", (chunk) => { chunks.push(chunk); writeStream.write(chunk); }); - - processedStream.on('end', () => { + + processedStream.on("end", () => { writeStream.end(); // Save processed data to lastScreencapData when done lastScreencapData = Buffer.concat(chunks); - + const totalFileOpTime = performance.now() - fileOpStartTime; const totalTime = performance.now() - startTime; - + emitLog("info", `โœ… Screenshot saved to ${filepath}`, "screencap"); emitLog("info", `โฑ๏ธ File save timing: ${totalFileOpTime.toFixed(2)}ms, Total: ${totalTime.toFixed(2)}ms`, "screencap"); - + emitter.emit("screencap-saved", { timestamp: new Date().toISOString(), filepath, timing: { fileOperation: totalFileOpTime, total: totalTime } }); - + emitter.emit("screencap-complete", { timestamp: new Date().toISOString(), filepath, @@ -1356,26 +1399,27 @@ export default async function createRemote(config) { height, timing: { fileOperation: totalFileOpTime, total: totalTime } }); - + resolve(); }); - - processedStream.on('error', (error) => { + + processedStream.on("error", (error) => { reject(error); }); }); - } catch (saveError) { // Show full error details for debugging emitLog("error", `๐Ÿ’ฅ [BACKGROUND SHARP] File save error: ${saveError.message}`, "screencap"); emitLog("error", `๐Ÿ’ฅ [BACKGROUND SHARP] Error stack: ${saveError.stack}`, "screencap"); - + // Only suppress specific libspng/PNG processing errors that occur after disconnection - if (!connected && saveError.message && ( - saveError.message.includes('libspng') || - saveError.message.includes('pngload_buffer') || - saveError.message.includes('read error') - )) { + if ( + !connected && + saveError.message && + (saveError.message.includes("libspng") || + saveError.message.includes("pngload_buffer") || + saveError.message.includes("read error")) + ) { emitLog("debug", `PNG processing error after disconnection (suppressed): ${filepath}`, "screencap"); } else { // Emit all other errors with full details @@ -1387,31 +1431,42 @@ export default async function createRemote(config) { backgroundOperations.delete(backgroundOperation); } })(); - + // Track the background operation - backgroundOperations.add(backgroundOperation); + backgroundOperations.add(backgroundOperation); emitLog("info", "Screenshot processing started in background", "screencap"); return; // Don't return a stream when saving to file - } else { // Return processed stream const processedStream = screencapStream.pipe(sharpTransform); const processEndTime = performance.now(); - + const streamCaptureTime = captureTime - startTime; const sharpProcessTime = processEndTime - processStartTime; const totalTime = processEndTime - startTime; - + // Debug timing variables - emitLog("debug", `๐Ÿ” Timing variables: captureTime=${captureTime}, startTime=${startTime}, processEndTime=${processEndTime}, processStartTime=${processStartTime}`, "screencap"); - + emitLog( + "debug", + `๐Ÿ” Timing variables: captureTime=${captureTime}, startTime=${startTime}, processEndTime=${processEndTime}, processStartTime=${processStartTime}`, + "screencap" + ); + emitLog("info", `PNG stream processed and ready`, "screencap"); try { - emitLog("info", `โฑ๏ธ Timing breakdown: Capture=${streamCaptureTime.toFixed(2)}ms, Sharp=${sharpProcessTime.toFixed(2)}ms, Total=${totalTime.toFixed(2)}ms`, "screencap"); + emitLog( + "info", + `โฑ๏ธ Timing breakdown: Capture=${streamCaptureTime.toFixed(2)}ms, Sharp=${sharpProcessTime.toFixed(2)}ms, Total=${totalTime.toFixed(2)}ms`, + "screencap" + ); } catch (timingError) { - emitLog("error", `โš ๏ธ Timing log error in stream processing: ${timingError.message}. Variables: streamCaptureTime=${streamCaptureTime}, sharpProcessTime=${sharpProcessTime}, totalTime=${totalTime}`, "screencap"); + emitLog( + "error", + `โš ๏ธ Timing log error in stream processing: ${timingError.message}. Variables: streamCaptureTime=${streamCaptureTime}, sharpProcessTime=${sharpProcessTime}, totalTime=${totalTime}`, + "screencap" + ); } - + // Emit ready event emitter.emit("screencap-ready", { timestamp: new Date().toISOString(), @@ -1424,7 +1479,7 @@ export default async function createRemote(config) { total: totalTime } }); - + // Emit complete event emitter.emit("screencap-complete", { timestamp: new Date().toISOString(), @@ -1437,16 +1492,15 @@ export default async function createRemote(config) { total: totalTime } }); - + // Store the processed stream for user access lastScreencapData = processedStream; - + return processedStream; } - } catch (error) { localEmitError(error, "screencap", "Failed to capture screenshot"); - + // Provide helpful error messages for common issues if (error.message && error.message.includes("unauthorized")) { emitLog("error", "Device unauthorized - ensure ADB debugging is enabled and device is authorized", "screencap"); @@ -1457,7 +1511,7 @@ export default async function createRemote(config) { } else if (error.message && error.message.includes("ENOENT")) { emitLog("error", `File path error: ${error.message}`, "screencap"); } - + throw error; } }, @@ -1481,13 +1535,13 @@ export default async function createRemote(config) { * @example * // Basic thumbnail with default 240px width * const thumbnailStream = await remote.thumbnail(); - * + * * // Thumbnail with custom dimensions * const customThumb = await remote.thumbnail({ width: 320, height: 180 }); - * + * * // Thumbnail saved to file * await remote.thumbnail({ filepath: './thumb.png' }); - * + * * // Thumbnail with custom size and file save * await remote.thumbnail({ width: 160, height: 90, filepath: './small-thumb.png' }); */ @@ -1497,9 +1551,9 @@ export default async function createRemote(config) { width: 240, ...options }; - - emitLog("info", `๐Ÿ“ธ Taking thumbnail screenshot (${thumbnailOptions.width}x${thumbnailOptions.height || 'auto'})`, "thumbnail"); - + + emitLog("info", `๐Ÿ“ธ Taking thumbnail screenshot (${thumbnailOptions.width}x${thumbnailOptions.height || "auto"})`, "thumbnail"); + // Call screencap with thumbnail options return await this.screencap(thumbnailOptions); }, @@ -1518,23 +1572,22 @@ export default async function createRemote(config) { async waitBootComplete(timeout = 60000) { try { emitLog("info", "=== Waiting for device boot completion ===", "waitBootComplete"); - + // Ensure we're connected before monitoring boot status await ensureConnected(); - + emitLog("info", `Monitoring boot status (timeout: ${timeout}ms)`, "waitBootComplete"); - + // Use the native device.waitBootComplete() method const result = await device.waitBootComplete(); - + emitLog("info", "โœ… Device boot completed successfully", "waitBootComplete"); emitLog("info", "Device is now ready for operations", "waitBootComplete"); return result; - } catch (error) { emitError(error, "waitBootComplete", "Failed while waiting for boot completion"); - + // Provide helpful error messages for common issues if (error.message && error.message.includes("unauthorized")) { emitLog("error", "Device unauthorized - ensure ADB debugging is enabled and device is authorized", "waitBootComplete"); @@ -1545,7 +1598,7 @@ export default async function createRemote(config) { } else if (error.message && error.message.includes("connection")) { emitLog("error", "Connection lost while waiting for boot completion", "waitBootComplete"); } - + throw error; } }, @@ -1748,7 +1801,7 @@ export default async function createRemote(config) { Object.keys(keyboardKeys).forEach((keyName) => { const char = keyboardKeys[keyName]; const shiftedChar = getShiftedCharacter(keyName, char); - + // Only create shift function if the character actually changes when shifted if (shiftedChar !== char) { /** @@ -1791,10 +1844,10 @@ export default async function createRemote(config) { * @public * @param {RemoteConfig} config - Configuration for the remote. * @returns {Promise} Resolves with a ready remote instance. - * + * * @description * Alias for createRemote() - both functions now return ready-to-use remote instances. - * + * * @example * // ESM with event handling * const remote = await createAndroidTVRemote({ ip: "192.168.1.100" }); diff --git a/tools/keyboard-test.mjs b/tools/keyboard-test.mjs index 8479cac..a288528 100644 --- a/tools/keyboard-test.mjs +++ b/tools/keyboard-test.mjs @@ -11,7 +11,7 @@ * @Copyright: Copyright (c) 2013-2025 Catalyzed Motivation Inc. All rights reserved. */ -import createRemote from '../src/lib/android-tv-remote.mjs'; +import createRemote from "../src/lib/android-tv-remote.mjs"; async function testKeyboardKeys() { console.log("๐Ÿ” Testing Keyboard Keys Available...");