refactor(server): replace Bun serve with Hono node adapters (#18335)

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
pull/21221/head
Dax 2026-04-06 13:24:55 -04:00 committed by GitHub
parent 4394e42615
commit 535343bf56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 364 additions and 152 deletions

View File

@ -329,8 +329,13 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
"@lydell/node-pty": "1.2.0-beta.10",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",
@ -1144,6 +1149,10 @@
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
"@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.6.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
"@gitlab/opencode-gitlab-auth": ["@gitlab/opencode-gitlab-auth@1.3.3", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
@ -1156,7 +1165,9 @@
"@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="], "@hey-api/types": ["@hey-api/types@0.1.2", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="], "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
@ -1348,6 +1359,20 @@
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
"@lydell/node-pty": ["@lydell/node-pty@1.2.0-beta.10", "", { "optionalDependencies": { "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10", "@lydell/node-pty-darwin-x64": "1.2.0-beta.10", "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10" } }, "sha512-Fv+A3+MZVA8qhkBIZsM1E6dCdHNMyXXz22mAYiMWd03LlyK///F3OH6CKPX9mj4id7LUlxpr45yPzyBVy9aDPw=="],
"@lydell/node-pty-darwin-arm64": ["@lydell/node-pty-darwin-arm64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C+eqDyRNHRYvx7RaHj6VVCx6nCpRBPuuxhTcc3JH3GuBMoxTsYeY4GkWH2XOktrgbAq1BG8e/Y8bu/wNQreCEw=="],
"@lydell/node-pty-darwin-x64": ["@lydell/node-pty-darwin-x64@1.2.0-beta.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-aZoIK6HtJO5BiT4ELm683U4dyHtt8b7wNgq3NJqYAQwSXrcPv576Z8vY3BIulVxfcFkht/SPLKou9TtdFXdNpg=="],
"@lydell/node-pty-linux-arm64": ["@lydell/node-pty-linux-arm64@1.2.0-beta.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0cKX2iMyXFNBE4fGtGK6B7IkdXcDMZajyEDoGMOgQQs/DDtoI5tSPcBcqNY9VitVrsRQA8+gFt6eKYU9Ye/lUA=="],
"@lydell/node-pty-linux-x64": ["@lydell/node-pty-linux-x64@1.2.0-beta.10", "", { "os": "linux", "cpu": "x64" }, "sha512-J9HnxvSzEeMH748+Ul1VrmCLWMo7iCVJy9EGijRR62+YO/Yk5GaCydUTZ+KzlH0/X5aTrgt5cfiof4vx45tRRg=="],
"@lydell/node-pty-win32-arm64": ["@lydell/node-pty-win32-arm64@1.2.0-beta.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-PlDJpJX/pnKyy6OmADKzhf+INZDDnzTBGaI0LT4laVNc6NblZNqUSkCMjLFWbeakeuQp0VG37M49WQSN9FDfeA=="],
"@lydell/node-pty-win32-x64": ["@lydell/node-pty-win32-x64@1.2.0-beta.10", "", { "os": "win32", "cpu": "x64" }, "sha512-ExFgWrzyldNAMi45U9PLIOu+g/RatP+f0c/dZxaooifME6yLW32BoHveH26/TtoAjZyJrc2iL0u48pgnR1fzmg=="],
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
"@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="],
@ -5194,10 +5219,18 @@
"@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "@fastify/proxy-addr/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
"@gitlab/gitlab-ai-provider/openai": ["openai@6.33.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
"@gitlab/gitlab-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@gitlab/opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
"@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@hono/node-ws/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=="],
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
@ -5250,6 +5283,8 @@
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="], "@modelcontextprotocol/sdk/hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
@ -6084,6 +6119,8 @@
"@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], "@expressive-code/plugin-shiki/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@gitlab/opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
"@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],

View File

@ -12,6 +12,7 @@
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook", "dev:storybook": "bun --cwd packages/storybook storybook",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky", "prepare": "husky",
"random": "echo 'Random script'", "random": "echo 'Random script'",
"hello": "echo 'Hello World!'", "hello": "echo 'Hello World!'",
@ -103,6 +104,7 @@
}, },
"trustedDependencies": [ "trustedDependencies": [
"esbuild", "esbuild",
"node-pty",
"protobufjs", "protobufjs",
"tree-sitter", "tree-sitter",
"tree-sitter-bash", "tree-sitter-bash",

View File

@ -87,7 +87,7 @@ const runnerEnv = {
let seed: ReturnType<typeof Bun.spawn> | undefined let seed: ReturnType<typeof Bun.spawn> | undefined
let runner: ReturnType<typeof Bun.spawn> | undefined let runner: ReturnType<typeof Bun.spawn> | undefined
let server: { stop: () => Promise<void> | void } | undefined let server: { stop: (close?: boolean) => Promise<void> | void } | undefined
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
let cleaned = false let cleaned = false
@ -100,7 +100,7 @@ const cleanup = async () => {
const jobs = [ const jobs = [
inst?.Instance.disposeAll(), inst?.Instance.disposeAll(),
server?.stop(), typeof server?.stop === "function" ? server.stop() : undefined,
keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }), keepSandbox ? undefined : fs.rm(sandbox, { recursive: true, force: true }),
].filter(Boolean) ].filter(Boolean)
await Promise.allSettled(jobs) await Promise.allSettled(jobs)
@ -158,7 +158,7 @@ try {
const servermod = await import("../../opencode/src/server/server") const servermod = await import("../../opencode/src/server/server")
inst = await import("../../opencode/src/project/instance") inst = await import("../../opencode/src/project/instance")
server = servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" }) server = await servermod.Server.listen({ port: serverPort, hostname: "127.0.0.1" })
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`) console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`) await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)

View File

@ -11,6 +11,7 @@
"test": "bun test --timeout 30000", "test": "bun test --timeout 30000",
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
"build": "bun run script/build.ts", "build": "bun run script/build.ts",
"fix-node-pty": "bun run script/fix-node-pty.ts",
"upgrade-opentui": "bun run script/upgrade-opentui.ts", "upgrade-opentui": "bun run script/upgrade-opentui.ts",
"dev": "bun run --conditions=browser ./src/index.ts", "dev": "bun run --conditions=browser ./src/index.ts",
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
@ -33,6 +34,11 @@
"bun": "./src/storage/db.bun.ts", "bun": "./src/storage/db.bun.ts",
"node": "./src/storage/db.node.ts", "node": "./src/storage/db.node.ts",
"default": "./src/storage/db.bun.ts" "default": "./src/storage/db.bun.ts"
},
"#pty": {
"bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.bun.ts"
} }
}, },
"devDependencies": { "devDependencies": {
@ -94,8 +100,13 @@
"@aws-sdk/credential-providers": "3.993.0", "@aws-sdk/credential-providers": "3.993.0",
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@gitlab/gitlab-ai-provider": "3.6.0",
"@gitlab/opencode-gitlab-auth": "1.3.3",
"@hono/node-server": "1.19.11",
"@hono/node-ws": "1.3.0",
"@hono/standard-validator": "0.1.5", "@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:", "@hono/zod-validator": "catalog:",
"@lydell/node-pty": "1.2.0-beta.10",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun"
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
@ -8,6 +9,15 @@ import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..") const dir = path.resolve(__dirname, "..")
const root = path.resolve(dir, "../..")
function linker(): "hoisted" | "isolated" {
// jsonc-parser is only declared in packages/opencode, so its install location
// tells us whether Bun used a hoisted or isolated workspace layout.
if (fs.existsSync(path.join(dir, "node_modules", "jsonc-parser"))) return "isolated"
if (fs.existsSync(path.join(root, "node_modules", "jsonc-parser"))) return "hoisted"
throw new Error("Could not detect Bun linker from jsonc-parser")
}
process.chdir(dir) process.chdir(dir)
@ -41,11 +51,16 @@ const migrations = await Promise.all(
) )
console.log(`Loaded ${migrations.length} migrations`) console.log(`Loaded ${migrations.length} migrations`)
const link = linker()
await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/node-pty@1.2.0-beta.10`
await Bun.build({ await Bun.build({
target: "node", target: "node",
entrypoints: ["./src/node.ts"], entrypoints: ["./src/node.ts"],
outdir: "./dist", outdir: "./dist",
format: "esm", format: "esm",
sourcemap: "linked",
external: ["jsonc-parser"], external: ["jsonc-parser"],
define: { define: {
OPENCODE_MIGRATIONS: JSON.stringify(migrations), OPENCODE_MIGRATIONS: JSON.stringify(migrations),

View File

@ -0,0 +1,28 @@
#!/usr/bin/env bun
import fs from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const dir = path.resolve(__dirname, "..")
if (process.platform !== "win32") {
const root = path.join(dir, "node_modules", "node-pty", "prebuilds")
const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => [])
const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper"))
const result = await Promise.all(
files.map(async (file) => {
const stat = await fs.stat(file).catch(() => undefined)
if (!stat) return
if ((stat.mode & 0o111) === 0o111) return
await fs.chmod(file, stat.mode | 0o755)
return file
}),
)
const fixed = result.filter(Boolean)
if (fixed.length) {
console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`)
}
}

View File

@ -23,7 +23,7 @@ export const AcpCommand = cmd({
process.env.OPENCODE_CLIENT = "acp" process.env.OPENCODE_CLIENT = "acp"
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
const opts = await resolveNetworkOptions(args) const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts) const server = await Server.listen(opts)
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`, baseUrl: `http://${server.hostname}:${server.port}`,

View File

@ -15,7 +15,7 @@ export const ServeCommand = cmd({
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
} }
const opts = await resolveNetworkOptions(args) const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts) const server = await Server.listen(opts)
console.log(`opencode server listening on http://${server.hostname}:${server.port}`) console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
await new Promise(() => {}) await new Promise(() => {})

View File

@ -37,7 +37,7 @@ export const WebCommand = cmd({
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
} }
const opts = await resolveNetworkOptions(args) const opts = await resolveNetworkOptions(args)
const server = Server.listen(opts) const server = await Server.listen(opts)
UI.empty() UI.empty()
UI.println(UI.logo(" ")) UI.println(UI.logo(" "))
UI.empty() UI.empty()

View File

@ -130,7 +130,8 @@ export namespace Plugin {
get serverUrl(): URL { get serverUrl(): URL {
return Server.url ?? new URL("http://localhost:4096") return Server.url ?? new URL("http://localhost:4096")
}, },
$: Bun.$, // @ts-expect-error
$: typeof Bun === "undefined" ? undefined : Bun.$,
} }
for (const plugin of INTERNAL_PLUGINS) { for (const plugin of INTERNAL_PLUGINS) {

View File

@ -3,7 +3,7 @@ import { Bus } from "@/bus"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { type IPty } from "bun-pty" import type { Proc } from "#pty"
import z from "zod" import z from "zod"
import { Log } from "../util/log" import { Log } from "../util/log"
import { lazy } from "@opencode-ai/util/lazy" import { lazy } from "@opencode-ai/util/lazy"
@ -26,9 +26,11 @@ export namespace Pty {
close: (code?: number, reason?: string) => void close: (code?: number, reason?: string) => void
} }
const sock = (ws: Socket) => (ws.data && typeof ws.data === "object" ? ws.data : ws)
type Active = { type Active = {
info: Info info: Info
process: IPty process: Proc
buffer: string buffer: string
bufferCursor: number bufferCursor: number
cursor: number cursor: number
@ -50,10 +52,7 @@ export namespace Pty {
return out return out
} }
const pty = lazy(async () => { const pty = lazy(() => import("#pty"))
const { spawn } = await import("bun-pty")
return spawn
})
export const Info = z export const Info = z
.object({ .object({
@ -124,9 +123,9 @@ export namespace Pty {
try { try {
session.process.kill() session.process.kill()
} catch {} } catch {}
for (const [key, ws] of session.subscribers.entries()) { for (const [sub, ws] of session.subscribers.entries()) {
try { try {
if (ws.data === key) ws.close() if (sock(ws) === sub) ws.close()
} catch {} } catch {}
} }
session.subscribers.clear() session.subscribers.clear()
@ -198,7 +197,7 @@ export namespace Pty {
} }
log.info("creating session", { id, cmd: command, args, cwd }) log.info("creating session", { id, cmd: command, args, cwd })
const spawn = yield* Effect.promise(() => pty()) const { spawn } = yield* Effect.promise(() => pty())
const proc = yield* Effect.sync(() => const proc = yield* Effect.sync(() =>
spawn(command, args, { spawn(command, args, {
name: "xterm-256color", name: "xterm-256color",
@ -234,7 +233,7 @@ export namespace Pty {
session.subscribers.delete(key) session.subscribers.delete(key)
continue continue
} }
if (ws.data !== key) { if (sock(ws) !== key) {
session.subscribers.delete(key) session.subscribers.delete(key)
continue continue
} }
@ -304,15 +303,12 @@ export namespace Pty {
} }
log.info("client connected to session", { id }) log.info("client connected to session", { id })
// Use ws.data as the unique key for this connection lifecycle. const sub = sock(ws)
// If ws.data is undefined, fallback to ws object. session.subscribers.delete(sub)
const key = ws.data && typeof ws.data === "object" ? ws.data : ws session.subscribers.set(sub, ws)
// Optionally cleanup if the key somehow exists
session.subscribers.delete(key)
session.subscribers.set(key, ws)
const cleanup = () => { const cleanup = () => {
session.subscribers.delete(key) session.subscribers.delete(sub)
} }
const start = session.bufferCursor const start = session.bufferCursor

View File

@ -0,0 +1,26 @@
import { spawn as create } from "bun-pty"
import type { Opts, Proc } from "./pty"
export type { Disp, Exit, Opts, Proc } from "./pty"
export function spawn(file: string, args: string[], opts: Opts): Proc {
const pty = create(file, args, opts)
return {
pid: pty.pid,
onData(listener) {
return pty.onData(listener)
},
onExit(listener) {
return pty.onExit(listener)
},
write(data) {
pty.write(data)
},
resize(cols, rows) {
pty.resize(cols, rows)
},
kill(signal) {
pty.kill(signal)
},
}
}

View File

@ -0,0 +1,27 @@
/** @ts-expect-error */
import * as pty from "@lydell/node-pty"
import type { Opts, Proc } from "./pty"
export type { Disp, Exit, Opts, Proc } from "./pty"
export function spawn(file: string, args: string[], opts: Opts): Proc {
const proc = pty.spawn(file, args, opts)
return {
pid: proc.pid,
onData(listener) {
return proc.onData(listener)
},
onExit(listener) {
return proc.onExit(listener)
},
write(data) {
proc.write(data)
},
resize(cols, rows) {
proc.resize(cols, rows)
},
kill(signal) {
proc.kill(signal)
},
}
}

View File

@ -0,0 +1,25 @@
export type Disp = {
dispose(): void
}
export type Exit = {
exitCode: number
signal?: number | string
}
export type Opts = {
name: string
cols?: number
rows?: number
cwd?: string
env?: Record<string, string>
}
export type Proc = {
pid: number
onData(listener: (data: string) => void): Disp
onExit(listener: (event: Exit) => void): Disp
write(data: string): void
resize(cols: number, rows: number): void
kill(signal?: string): void
}

View File

@ -1,6 +1,7 @@
import { describeRoute, resolver, validator } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import { Hono } from "hono" import { Hono } from "hono"
import { proxy } from "hono/proxy" import { proxy } from "hono/proxy"
import type { UpgradeWebSocket } from "hono/ws"
import z from "zod" import z from "zod"
import { createHash } from "node:crypto" import { createHash } from "node:crypto"
import { Log } from "../util/log" import { Log } from "../util/log"
@ -41,11 +42,11 @@ const DEFAULT_CSP =
const csp = (hash = "") => const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:` `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
export const InstanceRoutes = (app?: Hono) => export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) =>
(app ?? new Hono()) app
.onError(errorHandler(log)) .onError(errorHandler(log))
.route("/project", ProjectRoutes()) .route("/project", ProjectRoutes())
.route("/pty", PtyRoutes()) .route("/pty", PtyRoutes(upgrade))
.route("/config", ConfigRoutes()) .route("/config", ConfigRoutes())
.route("/experimental", ExperimentalRoutes()) .route("/experimental", ExperimentalRoutes())
.route("/session", SessionRoutes()) .route("/session", SessionRoutes())

View File

@ -1,4 +1,5 @@
import type { MiddlewareHandler } from "hono" import type { MiddlewareHandler } from "hono"
import type { UpgradeWebSocket } from "hono/ws"
import { getAdaptor } from "@/control-plane/adaptors" import { getAdaptor } from "@/control-plane/adaptors"
import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceID } from "@/control-plane/schema"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
@ -24,9 +25,10 @@ function local(method: string, path: string) {
return false return false
} }
const routes = lazy(() => InstanceRoutes()) export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
const routes = lazy(() => InstanceRoutes(upgrade))
export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { return async (c) => {
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = Filesystem.resolve( const directory = Filesystem.resolve(
(() => { (() => {
@ -97,3 +99,4 @@ export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
headers, headers,
}) })
} }
}

View File

@ -1,15 +1,14 @@
import { Hono } from "hono" import { Hono, type MiddlewareHandler } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi" import { describeRoute, validator, resolver } from "hono-openapi"
import { upgradeWebSocket } from "hono/bun" import type { UpgradeWebSocket } from "hono/ws"
import z from "zod" import z from "zod"
import { Pty } from "@/pty" import { Pty } from "@/pty"
import { PtyID } from "@/pty/schema" import { PtyID } from "@/pty/schema"
import { NotFoundError } from "../../storage/db" import { NotFoundError } from "../../storage/db"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy"
export const PtyRoutes = lazy(() => export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
new Hono() return new Hono()
.get( .get(
"/", "/",
describeRoute({ describeRoute({
@ -207,5 +206,5 @@ export const PtyRoutes = lazy(() =>
}, },
} }
}), }),
),
) )
}

View File

@ -4,12 +4,14 @@ import { Hono } from "hono"
import { compress } from "hono/compress" import { compress } from "hono/compress"
import { cors } from "hono/cors" import { cors } from "hono/cors"
import { basicAuth } from "hono/basic-auth" import { basicAuth } from "hono/basic-auth"
import type { UpgradeWebSocket } from "hono/ws"
import z from "zod" import z from "zod"
import { Auth } from "../auth" import { Auth } from "../auth"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { ProviderID } from "../provider/schema" import { ProviderID } from "../provider/schema"
import { createAdaptorServer, type ServerType } from "@hono/node-server"
import { createNodeWebSocket } from "@hono/node-ws"
import { WorkspaceRouterMiddleware } from "./router" import { WorkspaceRouterMiddleware } from "./router"
import { websocket } from "hono/bun"
import { errors } from "./error" import { errors } from "./error"
import { GlobalRoutes } from "./routes/global" import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns" import { MDNS } from "./mdns"
@ -24,8 +26,14 @@ globalThis.AI_SDK_LOG_WARNINGS = false
initProjectors() initProjectors()
export namespace Server { export namespace Server {
const log = Log.create({ service: "server" }) export type Listener = {
hostname: string
port: number
url: URL
stop: (close?: boolean) => Promise<void>
}
const log = Log.create({ service: "server" })
const zipped = compress() const zipped = compress()
const skipCompress = (path: string, method: string) => { const skipCompress = (path: string, method: string) => {
@ -34,10 +42,9 @@ export namespace Server {
return false return false
} }
export const Default = lazy(() => ControlPlaneRoutes()) export const Default = lazy(() => create({}).app)
export const ControlPlaneRoutes = (opts?: { cors?: string[] }): Hono => { export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
const app = new Hono()
return app return app
.onError(errorHandler(log)) .onError(errorHandler(log))
.use((c, next) => { .use((c, next) => {
@ -62,9 +69,7 @@ export namespace Server {
path: c.req.path, path: c.req.path,
}) })
await next() await next()
if (!skip) { if (!skip) timer.stop()
timer.stop()
}
}) })
.use( .use(
cors({ cors({
@ -81,15 +86,8 @@ export namespace Server {
) )
return input return input
// *.opencode.ai (https only, adjust if needed) if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) { if (opts?.cors?.includes(input)) return input
return input
}
if (opts?.cors?.includes(input)) {
return input
}
return
}, },
}), }),
) )
@ -234,11 +232,20 @@ export namespace Server {
return c.json(true) return c.json(true)
}, },
) )
.use(WorkspaceRouterMiddleware) .use(WorkspaceRouterMiddleware(upgrade))
}
function create(opts: { cors?: string[] }) {
const app = new Hono()
const ws = createNodeWebSocket({ app })
return {
app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
ws,
}
} }
export function createApp(opts: { cors?: string[] }) { export function createApp(opts: { cors?: string[] }) {
return ControlPlaneRoutes(opts) return create(opts).app
} }
export async function openapi() { export async function openapi() {
@ -246,8 +253,8 @@ export namespace Server {
// hono-openapi can see describeRoute metadata (`.route()` wraps // hono-openapi can see describeRoute metadata (`.route()` wraps
// handlers when the sub-app has a custom errorHandler, which // handlers when the sub-app has a custom errorHandler, which
// strips the metadata symbol). // strips the metadata symbol).
const app = ControlPlaneRoutes() const { app, ws } = create({})
InstanceRoutes(app) InstanceRoutes(ws.upgradeWebSocket, app)
const result = await generateSpecs(app, { const result = await generateSpecs(app, {
documentation: { documentation: {
info: { info: {
@ -261,52 +268,86 @@ export namespace Server {
return result return result
} }
/** @deprecated do not use this dumb shit */
export let url: URL export let url: URL
export function listen(opts: { export async function listen(opts: {
port: number port: number
hostname: string hostname: string
mdns?: boolean mdns?: boolean
mdnsDomain?: string mdnsDomain?: string
cors?: string[] cors?: string[]
}) { }): Promise<Listener> {
url = new URL(`http://${opts.hostname}:${opts.port}`) const built = create(opts)
const app = ControlPlaneRoutes({ cors: opts.cors }) const start = (port: number) =>
const args = { new Promise<ServerType>((resolve, reject) => {
hostname: opts.hostname, const server = createAdaptorServer({ fetch: built.app.fetch })
idleTimeout: 0, built.ws.injectWebSocket(server)
fetch: app.fetch, const fail = (err: Error) => {
websocket: websocket, cleanup()
} as const reject(err)
const tryServe = (port: number) => {
try {
return Bun.serve({ ...args, port })
} catch {
return undefined
} }
const ready = () => {
cleanup()
resolve(server)
} }
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) const cleanup = () => {
if (!server) throw new Error(`Failed to start server on port ${opts.port}`) server.off("error", fail)
server.off("listening", ready)
}
server.once("error", fail)
server.once("listening", ready)
server.listen(port, opts.hostname)
})
const shouldPublishMDNS = const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
const addr = server.address()
if (!addr || typeof addr === "string") {
throw new Error(`Failed to resolve server address for port ${opts.port}`)
}
const next = new URL("http://localhost")
next.hostname = opts.hostname
next.port = String(addr.port)
url = next
const mdns =
opts.mdns && opts.mdns &&
server.port && addr.port &&
opts.hostname !== "127.0.0.1" && opts.hostname !== "127.0.0.1" &&
opts.hostname !== "localhost" && opts.hostname !== "localhost" &&
opts.hostname !== "::1" opts.hostname !== "::1"
if (shouldPublishMDNS) { if (mdns) {
MDNS.publish(server.port!, opts.mdnsDomain) MDNS.publish(addr.port, opts.mdnsDomain)
} else if (opts.mdns) { } else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
} }
const originalStop = server.stop.bind(server) let closing: Promise<void> | undefined
server.stop = async (closeActiveConnections?: boolean) => { return {
if (shouldPublishMDNS) MDNS.unpublish() hostname: opts.hostname,
return originalStop(closeActiveConnections) port: addr.port,
url: next,
stop(close?: boolean) {
closing ??= new Promise((resolve, reject) => {
if (mdns) MDNS.unpublish()
server.close((err) => {
if (err) {
reject(err)
return
}
resolve()
})
if (close) {
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
server.closeAllConnections()
}
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
server.closeIdleConnections()
}
}
})
return closing
},
} }
return server
} }
} }