diff --git a/.github/settings.yml b/.github/settings.yml index 3f71f2b..133949f 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,22 +1,17 @@ # GitHub repository settings reference. -# These rules must be applied manually in GitHub → Settings → Branches. +# These rules must be applied manually via: gh api repos/jesse23/wtty/branches/main/protection # They are documented here so the intended configuration is version-controlled. # -# To apply: Settings → Branches → Add branch ruleset → target "main" -# # Branch protection for: main # # required_status_checks: -# strict: true # branch must be up to date before merge -# contexts: -# - lint -# - build -# - test +# strict: true +# contexts: [lint, build, test] # # required_pull_request_reviews: # required_approving_review_count: 1 -# dismiss_stale_reviews: true # re-review required after new push -# require_code_owner_reviews: true # enforces CODEOWNERS (jesse23 only) +# dismiss_stale_reviews: true +# require_code_owner_reviews: true # # allow_force_pushes: false # allow_deletions: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 232df72..9b48bfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,4 +34,4 @@ jobs: with: bun-version: 1.3.10 - run: bun install --frozen-lockfile - - run: bun test + - run: bun run test diff --git a/.github/workflows/copilot-review.yml b/.github/workflows/copilot-review.yml new file mode 100644 index 0000000..80ab057 --- /dev/null +++ b/.github/workflows/copilot-review.yml @@ -0,0 +1,18 @@ +name: Copilot Review + +on: + pull_request: + branches: + - main + +jobs: + copilot-review: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Request Copilot review + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr edit ${{ github.event.pull_request.number }} --add-reviewer "Copilot" diff --git a/biome.json b/biome.json index f6a62d8..85f7eed 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/bun.lock b/bun.lock index aa124bd..ad98f81 100644 --- a/bun.lock +++ b/bun.lock @@ -4,42 +4,125 @@ "workspaces": { "": { "name": "wtty", + "dependencies": { + "@lydell/node-pty": "1.2.0-beta.3", + "ghostty-web": "0.4.0-next.14.g6a1a50d", + "ws": "8.20.0", + }, "devDependencies": { - "@biomejs/biome": "2.4.4", - "@types/bun": "1.3.9", + "@biomejs/biome": "2.4.8", + "@types/bun": "1.3.11", + "@types/ws": "8.18.1", "rimraf": "6.1.3", + "tsx": "4.21.0", "typescript": "5.9.3", }, }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="], + "@biomejs/biome": ["@biomejs/biome@2.4.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.8", "@biomejs/cli-darwin-x64": "2.4.8", "@biomejs/cli-linux-arm64": "2.4.8", "@biomejs/cli-linux-arm64-musl": "2.4.8", "@biomejs/cli-linux-x64": "2.4.8", "@biomejs/cli-linux-x64-musl": "2.4.8", "@biomejs/cli-win32-arm64": "2.4.8", "@biomejs/cli-win32-x64": "2.4.8" }, "bin": { "biome": "bin/biome" } }, "sha512-ponn0oKOky1oRXBV+rlSaUlixUxf1aZvWC19Z41zBfUOUesthrQqL3OtiAlSB1EjFjyWpn98Q64DHelhA6jNlA=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ARx0tECE8I7S2C2yjnWYLNbBdDoPdq3oyNLhMglmuctThwUsuzFWRKrHmIGwIRWKz0Mat9DuzLEDp52hGnrxGQ=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jg9/PsB9vDCJlANE8uhG7qDhb5w0Ix69D7XIIc8IfZPUoiPrbLm33k2Ig3NOJ/7nb3UbesFz3D1aDKm9DvzjhQ=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-5CdrsJct76XG2hpKFwXnEtlT1p+4g4yV+XvvwBpzKsTNLO9c6iLlAxwcae2BJ7ekPGWjNGw9j09T5KGPKKxQig=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zo9OhBQDJ3IBGPlqHiTISloo5H0+FBIpemqIJdW/0edJ+gEcLR+MZeZozcUyz3o1nXkVA7++DdRKQT0599j9jA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-PdKXspVEaMCQLjtZCn6vfSck/li4KX9KGwSDbZdgIqlrizJ2MnMcE3TvHa2tVfXNmbjMikzcfJpuPWH695yJrw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.8", "", { "os": "linux", "cpu": "x64" }, "sha512-Gi8quv8MEuDdKaPFtS2XjEnMqODPsRg6POT6KhoP+VrkNb+T2ywunVB+TvOU0LX1jAZzfBr+3V1mIbBhzAMKvw=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-LoFatS0tnHv6KkCVpIy3qZCih+MxUMvdYiPWLHRri7mhi2vyOOs8OrbZBcLTUEWCS+ktO72nZMy4F96oMhkOHQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-vAn7iXDoUbqFXqVocuq1sMYAd33p8+mmurqJkWl6CtIhobd/O6moe4rY5AJvzbunn/qZCdiDVcveqtkFh1e7Hg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + + "@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.3", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.3", "@lydell/node-pty-darwin-x64": "1.2.0-beta.3", "@lydell/node-pty-linux-arm64": "1.2.0-beta.3", "@lydell/node-pty-linux-x64": "1.2.0-beta.3", "@lydell/node-pty-win32-arm64": "1.2.0-beta.3", "@lydell/node-pty-win32-x64": "1.2.0-beta.3" } }, "sha512-ngGAItlRhmJXrhspxt8kX13n1dVFqzETOq0m/+gqSkO8NJBvNMwP7FZckMwps2UFySdr4yxCXNGu/bumg5at6A=="], + + "@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-owcv+e1/OSu3bf9ZBdUQqJsQF888KyuSIiPYFNn0fLhgkhm9F3Pvha76Kj5mCPnodf7hh3suDe7upw7GPRXftQ=="], + + "@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-k38O+UviWrWdxtqZBBc/D8NJU11Rey8Y2YMwSWNxLv3eXZZdF5IVpbBkI/2RmLsV5nCcciqLPbukxeZnEfPlwA=="], + + "@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-HUwRpGu3O+4sv9DAQFKnyW5LYhyYu2SDUa/bdFO/t4dIFCM4uDJEq47wfRM7+aYtJTi1b3lakN8SlWeuFQqJQQ=="], + + "@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.3", "", { "os": "linux", "cpu": "x64" }, "sha512-+RRY0PoCUeQaCvPR7/UnkGbxulwbFtoTWJfe+o4T1RcNtngrgaI55I9nl8CD8uqhGrB3smKuyvPM5UtwGhASUw=="], + + "@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-UEDd9ASp2M3iIYpIzfmfBlpyn4+K1G4CAjYcHWStptCkefoSVXWTiUBIa1KjBjZi3/xmsHIDpBEYTkGWuvLt2Q=="], + + "@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.3", "", { "os": "win32", "cpu": "x64" }, "sha512-TpdqSFYx7/Rj+68tuP6F/lkRYrHCYAIJgaS1bx3SctTkb5QAQCFwOKHd4xlsivmEOMT2LdhkJggPxwX9PAO5pQ=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "ghostty-web": ["ghostty-web@0.4.0-next.14.g6a1a50d", "", {}, "sha512-01H59pAKq43Y8ZZiIhH98cu0ahZaoGoPQPtW4KnZPT0JFdQYen2djsXrOnt27JnMv64D4eLeAq8D6h72mo52xQ=="], "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], @@ -53,10 +136,16 @@ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], } } diff --git a/docs/adrs/001.wtty.bootstrap.md b/docs/adrs/001.wtty.bootstrap.md new file mode 100644 index 0000000..682f386 --- /dev/null +++ b/docs/adrs/001.wtty.bootstrap.md @@ -0,0 +1,70 @@ +# ADR 001: Bootstrap — Port ghostty-web demo + +**SPEC:** wtty +**Status:** Accepted +**Date:** 2026-03-21 + +--- + +## Context + +The first slice of wtty is a direct port of the `ghostty-web` demo. The demo already proves the full round-trip — browser renders a terminal, user types, a real PTY responds — so rather than building from scratch, we take that working implementation and make it the foundation of wtty. Everything else (config, multi-session, CLI) is deferred. + +## Decision + +Port `ghostty-web/demo` into a single entry point (`src/server.ts`). The server serves everything on one port — HTML, assets, and WebSocket — with all config hardcoded inline. + +**Server (`src/server.ts`):** +- HTTP server on port `2346` +- Serves the terminal HTML inline as a template string at `/` +- Serves `/dist/` assets (ghostty-web JS + WASM) from the installed `ghostty-web` package via `require.resolve` +- WebSocket endpoint at `/ws?cols=&rows=` — spawns one PTY per connection +- PTY: auto-detects shell (`$SHELL` on macOS/Linux, `cmd.exe` on Windows), cwd `$HOME` +- WebSocket message framing: raw string for PTY input; JSON `{ type: "resize", cols, rows }` for resize +- On PTY exit: sends exit message and closes WebSocket + +**PTY layer (`src/pty/`):** +- `types.ts` — `PtyProcess` interface shared by both adapters +- `bun.ts` — `Bun.Terminal` adapter (Bun v1.3.5+, native, no native addon) +- `node.ts` — `@lydell/node-pty` adapter (Node.js) +- `index.ts` — detects runtime via `process.versions.bun`, dynamically imports the correct adapter + +**Browser (inline HTML template in `src/server.ts`):** +- Full-viewport terminal with macOS-style title bar, no surrounding chrome +- `ghostty-web` (`init` + `Terminal` + `FitAddon`) as the terminal renderer +- Hardcoded config: `fontSize: 14`, `fontFamily: 'FiraMono Nerd Font, Menlo, Monaco, Courier New, monospace'` +- Theme: Dracula (official) — `background: #282A36`, `foreground: #F8F8F2`, `cursor: #F8F8F2`, `selection: #44475A`, `black: #21222C`, `red: #FF5555`, `green: #50FA7B`, `yellow: #F1FA8C`, `blue: #BD93F9`, `purple: #FF79C6`, `cyan: #8BE9FD`, `white: #F8F8F2`, `brightBlack: #6272A4`, `brightRed: #FF6E6E`, `brightGreen: #69FF94`, `brightYellow: #FFFFA5`, `brightBlue: #D6ACFF`, `brightPurple: #FF92DF`, `brightCyan: #A4FFFF`, `brightWhite: #FFFFFF` +- `FitAddon.fit()` + `observeResize()` for auto-resize +- WebSocket connects to same origin at `/ws` +- On resize: sends JSON resize message +- On close: reconnects after 2s + +**Dependencies:** +- `@lydell/node-pty@1.1.0` — cross-platform PTY for Node.js (fork with prebuilt binaries, no node-gyp) +- `ws@8.18.3` — WebSocket server +- `ghostty-web@0.4.0-next` — WASM terminal emulator, ported directly from the demo + +## Considered Options + +**Option A: xterm.js instead of ghostty-web** +xterm.js is the industry standard (used by ttyd, VS Code, wetty). More documentation and addons. Rejected for this slice — ghostty-web is already the working reference, uses the same `Terminal`/`FitAddon` API, and porting it is the fastest path to a running terminal. + +**Option B: Separate frontend build (Vite/esbuild)** +Would enable TypeScript in the browser and hot reload. Deferred — the demo uses plain ` + +`; + +const httpServer = http.createServer((req, res) => { + const url = new URL(req.url ?? '/', `http://${req.headers.host}`); + const pathname = url.pathname; + + if (pathname === '/' || pathname === '/index.html') { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(HTML_TEMPLATE); + return; + } + + if (pathname.startsWith('/dist/')) { + serveFile(path.join(distPath, pathname.slice(6)), res); + return; + } + + if (pathname === '/ghostty-vt.wasm') { + serveFile(wasmPath, res); + return; + } + + res.writeHead(404); + res.end('Not Found'); +}); + +function serveFile(filePath: string, res: http.ServerResponse): void { + const contentType = mimeType(filePath); + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end('Not Found'); + return; + } + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); +} + +const sessions = new Map(); + +function createPtySession(cols: number, rows: number): PtyProcess { + const shell = + process.platform === 'win32' + ? (process.env.COMSPEC ?? 'cmd.exe') + : (process.env.SHELL ?? '/bin/bash'); + return spawnPty(shell, cols, rows); +} + +const wss = new WebSocketServer({ noServer: true }); + +httpServer.on('upgrade', (req, socket, head) => { + const url = new URL(req.url ?? '/', `http://${req.headers.host}`); + if (url.pathname === '/ws') { + wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req)); + } else { + socket.destroy(); + } +}); + +wss.on('connection', (ws: WS, req: http.IncomingMessage) => { + const url = new URL(req.url ?? '/', `http://${req.headers.host}`); + const cols = Number.parseInt(url.searchParams.get('cols') ?? '80', 10); + const rows = Number.parseInt(url.searchParams.get('rows') ?? '24', 10); + + const ptyProcess = createPtySession(cols, rows); + sessions.set(ws, { pty: ptyProcess }); + + ptyProcess.onData((data: string) => { + if (ws.readyState === ws.OPEN) ws.send(data, { binary: false }); + }); + + ptyProcess.onExit(({ exitCode }) => { + if (ws.readyState === ws.OPEN) { + ws.send(`\r\n\x1b[33mShell exited (code: ${exitCode})\x1b[0m\r\n`); + ws.close(); + } + }); + + ws.on('message', (data: Buffer) => { + const message = data.toString('utf8'); + if (message.startsWith('{')) { + try { + const msg = JSON.parse(message) as { type: string; cols: number; rows: number }; + if (msg.type === 'resize') { + ptyProcess.resize(msg.cols, msg.rows); + return; + } + } catch { + // fall through + } + } + ptyProcess.write(message); + }); + + ws.on('close', () => { + sessions.get(ws)?.pty.kill(); + sessions.delete(ws); + }); + + ws.on('error', () => {}); + + const C = '\x1b[1;36m'; + const G = '\x1b[1;32m'; + const Y = '\x1b[1;33m'; + const R = '\x1b[0m'; + ws.send(`${C}╔══════════════════════════════════════════════════════════════╗${R}\r\n`); + ws.send( + `${C}║${R} ${G}Welcome to wtty!${R} ${C}║${R}\r\n`, + ); + ws.send(`${C}║${R} ${C}║${R}\r\n`); + ws.send(`${C}║${R} You have a real shell session with full PTY support. ${C}║${R}\r\n`); + ws.send( + `${C}║${R} Try: ${Y}ls${R}, ${Y}cd${R}, ${Y}top${R}, ${Y}vim${R}, or any command! ${C}║${R}\r\n`, + ); + ws.send(`${C}╚══════════════════════════════════════════════════════════════╝${R}\r\n\r\n`); +}); + +process.on('SIGINT', () => { + console.log('\n\nShutting down...'); + for (const [ws, session] of sessions.entries()) { + session.pty.kill(); + ws.close(); + } + wss.close(); + process.exit(0); +}); + +httpServer.listen(HTTP_PORT, () => { + console.log(`listening on http://localhost:${HTTP_PORT}`); +}); diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..7485561 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from 'bun:test'; +import { ghosttyWebRootFromMain, mimeType } from './utils'; + +describe('mimeType', () => { + test('returns correct type for known extensions', () => { + expect(mimeType('index.html')).toBe('text/html'); + expect(mimeType('app.js')).toBe('application/javascript'); + expect(mimeType('app.mjs')).toBe('application/javascript'); + expect(mimeType('style.css')).toBe('text/css'); + expect(mimeType('data.json')).toBe('application/json'); + expect(mimeType('module.wasm')).toBe('application/wasm'); + expect(mimeType('image.png')).toBe('image/png'); + expect(mimeType('icon.svg')).toBe('image/svg+xml'); + expect(mimeType('favicon.ico')).toBe('image/x-icon'); + }); + + test('returns octet-stream for unknown extensions', () => { + expect(mimeType('file.xyz')).toBe('application/octet-stream'); + expect(mimeType('binary.bin')).toBe('application/octet-stream'); + }); + + test('works with full paths', () => { + expect(mimeType('/dist/ghostty-web.js')).toBe('application/javascript'); + expect(mimeType('/dist/ghostty-vt.wasm')).toBe('application/wasm'); + }); +}); + +describe('ghosttyWebRootFromMain', () => { + test('strips dist/ and filename on posix path', () => { + expect(ghosttyWebRootFromMain('/node_modules/ghostty-web/dist/index.js')).toBe( + '/node_modules/ghostty-web', + ); + }); + + test('strips nested dist/ path', () => { + expect(ghosttyWebRootFromMain('/node_modules/ghostty-web/dist/ghostty-web.js')).toBe( + '/node_modules/ghostty-web', + ); + }); + + test('strips windows-style path', () => { + expect(ghosttyWebRootFromMain('C:\\node_modules\\ghostty-web\\dist\\index.js')).toBe( + 'C:\\node_modules\\ghostty-web', + ); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..9334dad --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,22 @@ +import path from 'node:path'; + +export const MIME_TYPES: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.mjs': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.wasm': 'application/wasm', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', +}; + +export function mimeType(filePath: string): string { + const ext = path.extname(filePath); + return MIME_TYPES[ext] ?? 'application/octet-stream'; +} + +export function ghosttyWebRootFromMain(mainPath: string): string { + return mainPath.replace(/[/\\]dist[/\\].*$/, ''); +} diff --git a/tsconfig.dts.json b/tsconfig.dts.json deleted file mode 100644 index 5be8964..0000000 --- a/tsconfig.dts.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts"] -} diff --git a/tsconfig.json b/tsconfig.json index 4009b51..8be0206 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,10 +8,8 @@ "resolveJsonModule": true, "esModuleInterop": true, "isolatedModules": true, - "declaration": true, - "declarationMap": true, "sourceMap": true, - "outDir": "./dist" + "noEmit": true }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/tsconfig.lint.json b/tsconfig.lint.json index 8f77925..7ce2f43 100644 --- a/tsconfig.lint.json +++ b/tsconfig.lint.json @@ -1,7 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "noEmit": true + "noEmit": true, + "moduleResolution": "node" }, "include": ["src", "scripts"] }