node v2 cli support (#36309)
This commit is contained in:
@@ -121,6 +121,53 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
version: ${{ needs.version.outputs.version }}
|
version: ${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
|
build-node-cli:
|
||||||
|
needs: version
|
||||||
|
if: github.repository == 'anomalyco/opencode'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
settings:
|
||||||
|
- target: linux-arm64
|
||||||
|
host: blacksmith-4vcpu-ubuntu-2404-arm
|
||||||
|
- target: linux-x64
|
||||||
|
host: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
- target: darwin-arm64
|
||||||
|
host: macos-26
|
||||||
|
- target: windows-arm64
|
||||||
|
host: windows-2025
|
||||||
|
- target: windows-x64
|
||||||
|
host: blacksmith-4vcpu-windows-2025
|
||||||
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "26.4.0"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun packages/cli/script/build-node.ts --target=${{ matrix.settings.target }} --outdir=dist/node
|
||||||
|
env:
|
||||||
|
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
|
||||||
|
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
|
||||||
|
|
||||||
|
- name: Verify service lifecycle
|
||||||
|
if: matrix.settings.target != 'windows-arm64'
|
||||||
|
working-directory: packages/cli
|
||||||
|
run: bun run script/service-smoke.ts --node
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: opencode-node-cli-${{ matrix.settings.target }}
|
||||||
|
path: packages/cli/dist/node/cli-node-*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
sign-cli-windows:
|
sign-cli-windows:
|
||||||
needs:
|
needs:
|
||||||
- build-cli
|
- build-cli
|
||||||
@@ -413,6 +460,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
- build-cli
|
- build-cli
|
||||||
|
- build-node-cli
|
||||||
- sign-cli-windows
|
- sign-cli-windows
|
||||||
- build-electron
|
- build-electron
|
||||||
if: always() && !failure() && !cancelled()
|
if: always() && !failure() && !cancelled()
|
||||||
@@ -461,6 +509,12 @@ jobs:
|
|||||||
name: opencode-preview-cli
|
name: opencode-preview-cli
|
||||||
path: packages/cli/dist
|
path: packages/cli/dist
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
|
with:
|
||||||
|
pattern: opencode-node-cli-*
|
||||||
|
path: packages/cli/dist/node
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
if: needs.version.outputs.release
|
if: needs.version.outputs.release
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -78,6 +78,20 @@ jobs:
|
|||||||
bun run script/build.ts --single --skip-install
|
bun run script/build.ts --single --skip-install
|
||||||
bun run script/service-smoke.ts
|
bun run script/service-smoke.ts
|
||||||
|
|
||||||
|
- name: Setup Node build runtime
|
||||||
|
if: always()
|
||||||
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||||
|
with:
|
||||||
|
node-version: "26.4.0"
|
||||||
|
|
||||||
|
- name: Verify Node build
|
||||||
|
if: always()
|
||||||
|
timeout-minutes: 15
|
||||||
|
working-directory: packages/cli
|
||||||
|
run: |
|
||||||
|
bun run script/build-node.ts --single --skip-install --outdir=dist/node
|
||||||
|
bun run script/service-smoke.ts --node
|
||||||
|
|
||||||
- name: Check generated client
|
- name: Check generated client
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
working-directory: packages/client
|
working-directory: packages/client
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ node_modules
|
|||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
dist
|
dist
|
||||||
|
dist-node
|
||||||
ts-dist
|
ts-dist
|
||||||
.turbo
|
.turbo
|
||||||
.typecheck-profiles
|
.typecheck-profiles
|
||||||
@@ -25,6 +26,7 @@ Session.vim
|
|||||||
a.out
|
a.out
|
||||||
target
|
target
|
||||||
.scripts
|
.scripts
|
||||||
|
.cache
|
||||||
.direnv/
|
.direnv/
|
||||||
|
|
||||||
# Local dev files
|
# Local dev files
|
||||||
|
|||||||
@@ -145,14 +145,28 @@
|
|||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3",
|
"uqr": "0.1.3",
|
||||||
|
"ws": "8.21.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-x64": "2.5.1",
|
||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"vite": "catalog:",
|
||||||
|
"vite-plugin-solid": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/client": {
|
"packages/client": {
|
||||||
@@ -387,6 +401,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
|
"resolve.exports": "catalog:",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.1.1",
|
||||||
@@ -739,9 +754,9 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.4.3",
|
"@opentui/core": ">=0.4.5",
|
||||||
"@opentui/keymap": ">=0.4.3",
|
"@opentui/keymap": ">=0.4.5",
|
||||||
"@opentui/solid": ">=0.4.3",
|
"@opentui/solid": ">=0.4.5",
|
||||||
},
|
},
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@opentui/core",
|
"@opentui/core",
|
||||||
@@ -1021,10 +1036,12 @@
|
|||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
"get-east-asian-width": "catalog:",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opentui-spinner": "catalog:",
|
"opentui-spinner": "catalog:",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
"string-width": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3",
|
"uqr": "0.1.3",
|
||||||
},
|
},
|
||||||
@@ -1190,9 +1207,9 @@
|
|||||||
"@npmcli/arborist": "9.4.0",
|
"@npmcli/arborist": "9.4.0",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opentui/core": "0.4.3",
|
"@opentui/core": "0.4.5",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.4.5",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.4.5",
|
||||||
"@pierre/diffs": "1.2.10",
|
"@pierre/diffs": "1.2.10",
|
||||||
"@playwright/test": "1.59.1",
|
"@playwright/test": "1.59.1",
|
||||||
"@sentry/solid": "10.36.0",
|
"@sentry/solid": "10.36.0",
|
||||||
@@ -1220,6 +1237,7 @@
|
|||||||
"drizzle-orm": "1.0.0-rc.2",
|
"drizzle-orm": "1.0.0-rc.2",
|
||||||
"effect": "4.0.0-beta.83",
|
"effect": "4.0.0-beta.83",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"get-east-asian-width": "1.6.0",
|
||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
@@ -1228,11 +1246,13 @@
|
|||||||
"opentui-spinner": "0.0.7",
|
"opentui-spinner": "0.0.7",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
"remend": "1.3.0",
|
"remend": "1.3.0",
|
||||||
|
"resolve.exports": "2.0.3",
|
||||||
"semver": "7.7.4",
|
"semver": "7.7.4",
|
||||||
"shiki": "4.2.0",
|
"shiki": "4.2.0",
|
||||||
"solid-js": "1.9.10",
|
"solid-js": "1.9.10",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"sst": "4.13.1",
|
"sst": "4.13.1",
|
||||||
|
"string-width": "7.2.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -2273,27 +2293,27 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="],
|
"@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
|
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
|
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg=="],
|
||||||
|
|
||||||
"@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="],
|
"@opentui/keymap": ["@opentui/keymap@0.4.5", "", { "dependencies": { "@opentui/core": "0.4.5" }, "peerDependencies": { "@opentui/react": "0.4.5", "@opentui/solid": "0.4.5", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="],
|
"@opentui/solid": ["@opentui/solid@0.4.5", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.5", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g=="],
|
||||||
|
|
||||||
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
|
||||||
|
|
||||||
@@ -5595,6 +5615,8 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||||
|
|
||||||
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
"responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||||
|
|||||||
+6
-3
@@ -46,9 +46,9 @@
|
|||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
"@hono/standard-validator": "0.2.0",
|
"@hono/standard-validator": "0.2.0",
|
||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@opentui/core": "0.4.3",
|
"@opentui/core": "0.4.5",
|
||||||
"@opentui/keymap": "0.4.3",
|
"@opentui/keymap": "0.4.5",
|
||||||
"@opentui/solid": "0.4.3",
|
"@opentui/solid": "0.4.5",
|
||||||
"@tanstack/solid-virtual": "3.13.32",
|
"@tanstack/solid-virtual": "3.13.32",
|
||||||
"@shikijs/stream": "4.2.0",
|
"@shikijs/stream": "4.2.0",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
"hono": "4.10.7",
|
"hono": "4.10.7",
|
||||||
"hono-openapi": "1.1.2",
|
"hono-openapi": "1.1.2",
|
||||||
"fuzzysort": "3.1.0",
|
"fuzzysort": "3.1.0",
|
||||||
|
"get-east-asian-width": "1.6.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "17.0.6",
|
"marked": "17.0.6",
|
||||||
"marked-shiki": "1.2.1",
|
"marked-shiki": "1.2.1",
|
||||||
@@ -85,9 +86,11 @@
|
|||||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
"remeda": "2.26.0",
|
"remeda": "2.26.0",
|
||||||
|
"resolve.exports": "2.0.3",
|
||||||
"sst": "4.13.1",
|
"sst": "4.13.1",
|
||||||
"shiki": "4.2.0",
|
"shiki": "4.2.0",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
|
"string-width": "7.2.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ function run(target) {
|
|||||||
|
|
||||||
const envPath = process.env.OPENCODE_BIN_PATH
|
const envPath = process.env.OPENCODE_BIN_PATH
|
||||||
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
const scriptDir = path.dirname(fs.realpathSync(__filename))
|
||||||
const cached = path.join(scriptDir, ".opencode2")
|
const command = path.basename(__filename).replace(/\.cjs$/, "")
|
||||||
|
const nodeBuild = command === "opencode2-node"
|
||||||
|
const cached = path.join(scriptDir, `.${command}`)
|
||||||
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
|
||||||
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
|
||||||
const base = "@opencode-ai/cli-" + platform + "-" + arch
|
const base = `@opencode-ai/cli${nodeBuild ? "-node" : ""}-` + platform + "-" + arch
|
||||||
const binary = platform === "windows" ? "opencode2.exe" : "opencode2"
|
const binary = platform === "windows" ? `${command}.exe` : command
|
||||||
|
|
||||||
function supportsAvx2() {
|
function supportsAvx2() {
|
||||||
if (arch !== "x64") return false
|
if (arch !== "x64") return false
|
||||||
@@ -77,6 +79,7 @@ function supportsAvx2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const names = (() => {
|
const names = (() => {
|
||||||
|
if (nodeBuild) return [base]
|
||||||
const baseline = arch === "x64" && !supportsAvx2()
|
const baseline = arch === "x64" && !supportsAvx2()
|
||||||
if (platform === "linux") {
|
if (platform === "linux") {
|
||||||
const musl = (() => {
|
const musl = (() => {
|
||||||
@@ -121,7 +124,7 @@ function findBinary(startDir) {
|
|||||||
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
console.error(
|
console.error(
|
||||||
"It seems that your package manager failed to install the right opencode2 CLI package. Try manually installing " +
|
`It seems that your package manager failed to install the right ${command} CLI package. Try manually installing ` +
|
||||||
names.map((name) => `"${name}"`).join(" or ") +
|
names.map((name) => `"${name}"`).join(" or ") +
|
||||||
" package",
|
" package",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run script/build.ts",
|
"build": "bun run script/build.ts",
|
||||||
|
"build:node": "bun run script/build-node.ts",
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
"test": "bun test --timeout 30000 --only-failures",
|
"test": "bun test --timeout 30000 --only-failures",
|
||||||
"typecheck": "tsgo --noEmit"
|
"typecheck": "tsgo --noEmit"
|
||||||
@@ -51,7 +52,8 @@
|
|||||||
"semver": "catalog:",
|
"semver": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3"
|
"uqr": "0.1.3",
|
||||||
|
"ws": "8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
@@ -59,6 +61,19 @@
|
|||||||
"@tsconfig/bun": "catalog:",
|
"@tsconfig/bun": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/semver": "catalog:",
|
"@types/semver": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:"
|
"@typescript/native-preview": "catalog:",
|
||||||
|
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||||
|
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||||
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-x64": "2.5.1",
|
||||||
|
"vite": "catalog:",
|
||||||
|
"vite-plugin-solid": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process"
|
||||||
|
import { createHash } from "node:crypto"
|
||||||
|
import { chmod, copyFile, mkdir, mkdtemp, realpath, rename, rm, stat, writeFile } from "node:fs/promises"
|
||||||
|
import os from "node:os"
|
||||||
|
import path from "node:path"
|
||||||
|
import { build } from "vite"
|
||||||
|
import { Script } from "@opencode-ai/script"
|
||||||
|
import pkg from "../package.json"
|
||||||
|
import { modelsData } from "./generate"
|
||||||
|
import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"
|
||||||
|
import { mainConfig } from "../vite.node.config"
|
||||||
|
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
|
||||||
|
|
||||||
|
const NODE_VERSION = "26.4.0"
|
||||||
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
|
const outdir = path.resolve(
|
||||||
|
dir,
|
||||||
|
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
|
||||||
|
)
|
||||||
|
if (outdir === dir) throw new Error("--outdir must not be the package directory")
|
||||||
|
if (outdir === path.join(dir, "dist-node")) {
|
||||||
|
throw new Error("--outdir must not be dist-node because it contains temporary files")
|
||||||
|
}
|
||||||
|
const bundleOnly = process.argv.includes("--bundle-only")
|
||||||
|
const single = process.argv.includes("--single")
|
||||||
|
const skipInstall = process.argv.includes("--skip-install")
|
||||||
|
const requested = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length)
|
||||||
|
const allTargets = [
|
||||||
|
nodeTarget("linux", "arm64"),
|
||||||
|
nodeTarget("linux", "x64"),
|
||||||
|
nodeTarget("darwin", "arm64"),
|
||||||
|
nodeTarget("win32", "arm64"),
|
||||||
|
nodeTarget("win32", "x64"),
|
||||||
|
]
|
||||||
|
const targets = requested
|
||||||
|
? allTargets.filter((target) => targetName(target) === requested)
|
||||||
|
: single || bundleOnly
|
||||||
|
? [nodeTarget(process.platform, process.arch)]
|
||||||
|
: allTargets
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
if (requested === "darwin-x64") throw new Error("Node 26.4 SEA does not support macOS x64")
|
||||||
|
throw new Error(`Unknown Node target: ${requested}`)
|
||||||
|
}
|
||||||
|
if (!bundleOnly && targets.some((target) => target.platform === "darwin" && target.arch === "x64")) {
|
||||||
|
throw new Error("Node 26.4 SEA does not support macOS x64")
|
||||||
|
}
|
||||||
|
|
||||||
|
process.chdir(dir)
|
||||||
|
if (!skipInstall) run(process.execPath, ["install", "--os=*", "--cpu=*"])
|
||||||
|
if (!bundleOnly) await rm(outdir, { recursive: true, force: true })
|
||||||
|
const builder =
|
||||||
|
!bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch)
|
||||||
|
? await resolveHostNode()
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
console.log(`building cli-node-${targetName(target)}`)
|
||||||
|
const assets = await collectNodeAssets(target)
|
||||||
|
await rm("dist-node", { recursive: true, force: true })
|
||||||
|
const assetHash = await hashNodeAssets(assets)
|
||||||
|
const input = { version: Script.version, channel: Script.channel, models: modelsData, assetHash, target }
|
||||||
|
await build(mainConfig(input))
|
||||||
|
await copyNodeAssets(assets)
|
||||||
|
|
||||||
|
const host = target.platform === process.platform && target.arch === process.arch
|
||||||
|
if (host) {
|
||||||
|
if (!builder) throw new Error("Node SEA builder is unavailable")
|
||||||
|
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--version"])
|
||||||
|
run(builder, [...nodeExecArgv, "dist-node/opencode.mjs", "--help"])
|
||||||
|
}
|
||||||
|
if (bundleOnly) continue
|
||||||
|
|
||||||
|
const name = `cli-node-${targetName(target)}`
|
||||||
|
const binary = target.platform === "win32" ? "opencode2-node.exe" : "opencode2-node"
|
||||||
|
const output = path.join(outdir, name, "bin", binary)
|
||||||
|
if (!builder) throw new Error("Node SEA builder is unavailable")
|
||||||
|
await mkdir(path.dirname(output), { recursive: true })
|
||||||
|
const config = {
|
||||||
|
main: "dist-node/opencode.mjs",
|
||||||
|
mainFormat: "module",
|
||||||
|
executable: await resolveTargetNode(target, builder),
|
||||||
|
output: path.relative(dir, output),
|
||||||
|
disableExperimentalSEAWarning: true,
|
||||||
|
useSnapshot: false,
|
||||||
|
useCodeCache: false,
|
||||||
|
execArgv: nodeExecArgv,
|
||||||
|
execArgvExtension: "none",
|
||||||
|
assets: await seaAssetMap(),
|
||||||
|
}
|
||||||
|
await writeFile("dist-node/sea.json", `${JSON.stringify(config, null, 2)}\n`)
|
||||||
|
run(builder, ["--build-sea", "dist-node/sea.json"])
|
||||||
|
if (target.platform !== "win32") await chmod(output, 0o755)
|
||||||
|
if (target.platform === "darwin" && process.platform === "darwin") run("codesign", ["--sign", "-", output])
|
||||||
|
if (target.platform === "darwin" && process.platform !== "darwin") {
|
||||||
|
console.warn(`${output} must be signed on macOS before it can run`)
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
path.join(outdir, name, "package.json"),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
name: `@opencode-ai/${name}`,
|
||||||
|
version: Script.version,
|
||||||
|
license: pkg.license,
|
||||||
|
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||||
|
os: [target.platform],
|
||||||
|
cpu: [target.arch],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
)
|
||||||
|
if (host) await smoke(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveHostNode() {
|
||||||
|
const candidates = [process.env.NODE_BIN, "node"].filter((item): item is string => Boolean(item))
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const result = spawnSync(
|
||||||
|
candidate,
|
||||||
|
["-p", "JSON.stringify({version:process.versions.node,path:process.execPath})"],
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (result.status !== 0) continue
|
||||||
|
const info = JSON.parse(result.stdout) as { version: string; path: string }
|
||||||
|
if (info.version === NODE_VERSION) return realpath(info.path)
|
||||||
|
}
|
||||||
|
return resolveTargetNode(nodeTarget(process.platform, process.arch))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveTargetNode(target: NodeTarget, host?: string) {
|
||||||
|
if (host && target.platform === process.platform && target.arch === process.arch) return host
|
||||||
|
const cache = path.resolve(dir, ".cache", "node")
|
||||||
|
const platform = target.platform === "win32" ? "win" : target.platform
|
||||||
|
const archiveName = `node-v${NODE_VERSION}-${platform}-${target.arch}`
|
||||||
|
const targetDirectory = path.join(cache, archiveName)
|
||||||
|
const executable = path.join(targetDirectory, target.platform === "win32" ? "node.exe" : "bin/node")
|
||||||
|
if (
|
||||||
|
(await stat(executable).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
)) &&
|
||||||
|
(await stat(path.join(targetDirectory, ".verified")).then(
|
||||||
|
() => true,
|
||||||
|
() => false,
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return realpath(executable)
|
||||||
|
await mkdir(cache, { recursive: true })
|
||||||
|
const extension = target.platform === "win32" ? "zip" : "tar.gz"
|
||||||
|
const filename = `${archiveName}.${extension}`
|
||||||
|
const archive = path.join(cache, filename)
|
||||||
|
const base = `https://nodejs.org/dist/v${NODE_VERSION}`
|
||||||
|
const [response, sums] = await Promise.all([fetch(`${base}/${filename}`), fetch(`${base}/SHASUMS256.txt`)])
|
||||||
|
if (!response.ok) throw new Error(`Failed to download Node ${NODE_VERSION}: ${response.status}`)
|
||||||
|
if (!sums.ok) throw new Error(`Failed to download Node ${NODE_VERSION} checksums: ${sums.status}`)
|
||||||
|
const data = new Uint8Array(await response.arrayBuffer())
|
||||||
|
const expected = (await sums.text())
|
||||||
|
.split("\n")
|
||||||
|
.find((line) => line.endsWith(` ${filename}`))
|
||||||
|
?.split(/\s+/)[0]
|
||||||
|
if (!expected) throw new Error(`Missing checksum for ${filename}`)
|
||||||
|
if (createHash("sha256").update(data).digest("hex") !== expected) throw new Error(`Checksum mismatch for ${filename}`)
|
||||||
|
await writeFile(archive, data)
|
||||||
|
const temporary = path.join(cache, `${archiveName}.${process.pid}.tmp`)
|
||||||
|
await rm(temporary, { recursive: true, force: true })
|
||||||
|
await mkdir(temporary)
|
||||||
|
if (target.platform !== "win32") run("tar", ["-xzf", archive, "-C", temporary])
|
||||||
|
else if (process.platform === "win32") run("tar", ["-xf", archive, "-C", temporary])
|
||||||
|
else run("unzip", ["-q", archive, "-d", temporary])
|
||||||
|
await rm(targetDirectory, { recursive: true, force: true })
|
||||||
|
await rename(path.join(temporary, archiveName), targetDirectory)
|
||||||
|
await writeFile(path.join(targetDirectory, ".verified"), `${expected}\n`)
|
||||||
|
await rm(temporary, { recursive: true, force: true })
|
||||||
|
await rm(archive, { force: true })
|
||||||
|
return realpath(executable)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function smoke(output: string) {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), "opencode-node-smoke-"))
|
||||||
|
const executable = path.join(root, path.basename(output))
|
||||||
|
await copyFile(output, executable)
|
||||||
|
if (process.platform !== "win32") await chmod(executable, 0o755)
|
||||||
|
run(executable, ["--version"], root)
|
||||||
|
run(executable, ["--help"], root)
|
||||||
|
await rm(root, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetName(target: NodeTarget) {
|
||||||
|
return `${target.platform === "win32" ? "windows" : target.platform}-${target.arch}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command: string, args: readonly string[], cwd = dir) {
|
||||||
|
const result = spawnSync(command, args, { cwd, stdio: "inherit", env: process.env })
|
||||||
|
if (result.error) throw result.error
|
||||||
|
if (result.status !== 0) throw new Error(`${command} exited with status ${result.status ?? "unknown"}`)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import fs from "fs"
|
|
||||||
import { rm } from "fs/promises"
|
import { rm } from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Script } from "@opencode-ai/script"
|
import { Script } from "@opencode-ai/script"
|
||||||
@@ -11,9 +10,14 @@ import { modelsData } from "./generate"
|
|||||||
|
|
||||||
const dir = path.resolve(import.meta.dirname, "..")
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
const binary = "opencode2"
|
const binary = "opencode2"
|
||||||
|
const outdir = path.resolve(
|
||||||
|
dir,
|
||||||
|
process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist",
|
||||||
|
)
|
||||||
|
if (outdir === dir) throw new Error("--outdir must not be the package directory")
|
||||||
process.chdir(dir)
|
process.chdir(dir)
|
||||||
|
|
||||||
await rm("dist", { recursive: true, force: true })
|
await rm(outdir, { recursive: true, force: true })
|
||||||
|
|
||||||
const singleFlag = process.argv.includes("--single")
|
const singleFlag = process.argv.includes("--single")
|
||||||
const baselineFlag = process.argv.includes("--baseline")
|
const baselineFlag = process.argv.includes("--baseline")
|
||||||
@@ -50,10 +54,6 @@ const targets = singleFlag
|
|||||||
|
|
||||||
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
|
||||||
|
|
||||||
const localParserWorker = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
|
|
||||||
const rootParserWorker = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
|
|
||||||
const parserWorker = fs.realpathSync(fs.existsSync(localParserWorker) ? localParserWorker : rootParserWorker)
|
|
||||||
|
|
||||||
for (const item of targets) {
|
for (const item of targets) {
|
||||||
const target = [
|
const target = [
|
||||||
binary,
|
binary,
|
||||||
@@ -67,7 +67,7 @@ for (const item of targets) {
|
|||||||
const name = target.replace(binary, "cli")
|
const name = target.replace(binary, "cli")
|
||||||
console.log(`building ${name}`)
|
console.log(`building ${name}`)
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/index.ts", parserWorker],
|
entrypoints: ["./src/index.ts"],
|
||||||
tsconfig: "./tsconfig.json",
|
tsconfig: "./tsconfig.json",
|
||||||
plugins: [plugin],
|
plugins: [plugin],
|
||||||
external: ["node-gyp"],
|
external: ["node-gyp"],
|
||||||
@@ -81,7 +81,7 @@ for (const item of targets) {
|
|||||||
autoloadTsconfig: true,
|
autoloadTsconfig: true,
|
||||||
autoloadPackageJson: true,
|
autoloadPackageJson: true,
|
||||||
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
|
||||||
outfile: `./dist/${name}/bin/${binary}`,
|
outfile: path.join(outdir, name, "bin", binary),
|
||||||
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
|
||||||
windows: {},
|
windows: {},
|
||||||
},
|
},
|
||||||
@@ -93,10 +93,6 @@ for (const item of targets) {
|
|||||||
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
|
||||||
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
// FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
|
||||||
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
|
||||||
OTUI_TREE_SITTER_WORKER_PATH:
|
|
||||||
(item.os === "win32" ? '"B:/~BUN/root/' : '"/$bunfs/root/') +
|
|
||||||
path.relative(dir, parserWorker).replaceAll("\\", "/") +
|
|
||||||
'"',
|
|
||||||
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -107,7 +103,7 @@ for (const item of targets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
`./dist/${name}/package.json`,
|
path.join(outdir, name, "package.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name: `@opencode-ai/${name}`,
|
name: `@opencode-ai/${name}`,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
|
||||||
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
|
||||||
|
|
||||||
export const modelsData = process.env.MODELS_DEV_API_JSON
|
export const modelsData = process.env.MODELS_DEV_API_JSON
|
||||||
? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
|
? await readFile(process.env.MODELS_DEV_API_JSON, "utf8")
|
||||||
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
: await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
|
||||||
|
|
||||||
console.log("Loaded models.dev snapshot")
|
console.log("Loaded models.dev snapshot")
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { createHash } from "node:crypto"
|
||||||
|
import { copyFile, mkdir, readdir, readFile, stat } from "node:fs/promises"
|
||||||
|
import { createRequire } from "node:module"
|
||||||
|
import path from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { getNodeAssets } from "@opentui/core/node-assets"
|
||||||
|
import { attentionSoundAssets, type NodeTarget, photonWasmAsset } from "../src/node/target"
|
||||||
|
|
||||||
|
const dir = path.resolve(import.meta.dirname, "..")
|
||||||
|
|
||||||
|
// Bun's compiler discovers file imports and embeds them in its virtual filesystem. Vite only bundles the JavaScript
|
||||||
|
// portion of the Node executable, while SEA embeds only the assets explicitly listed in its build configuration.
|
||||||
|
// Collect and stage those files under stable keys so the SEA prelude can extract them to real paths at startup;
|
||||||
|
// native addons, helper executables, and other path-based consumers cannot use assets directly from SEA memory.
|
||||||
|
export type NodeAsset = {
|
||||||
|
readonly key: string
|
||||||
|
readonly source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function files(root: string, current = root): Promise<string[]> {
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
(await readdir(current, { withFileTypes: true })).map((entry) => {
|
||||||
|
const target = path.join(current, entry.name)
|
||||||
|
return entry.isDirectory() ? files(root, target) : [path.relative(root, target)]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).flat()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collectNodeAssets(target: NodeTarget) {
|
||||||
|
const ptyEntry = fileURLToPath(import.meta.resolve(target.nodePtyPackage))
|
||||||
|
const ptyRoot = path.resolve(path.dirname(ptyEntry), "..")
|
||||||
|
const assets: NodeAsset[] = [
|
||||||
|
...getNodeAssets({
|
||||||
|
platform: target.platform,
|
||||||
|
arch: target.arch,
|
||||||
|
...(target.platform === "linux" ? { libc: "glibc" as const } : {}),
|
||||||
|
}),
|
||||||
|
{ key: target.parcelWatcherAsset, source: fileURLToPath(import.meta.resolve(target.parcelWatcherPackage)) },
|
||||||
|
{
|
||||||
|
key: photonWasmAsset,
|
||||||
|
source: createRequire(path.resolve(dir, "../core/package.json")).resolve(photonWasmAsset),
|
||||||
|
},
|
||||||
|
...attentionSoundAssets.map((key) => ({
|
||||||
|
key,
|
||||||
|
source: path.resolve(dir, "../ui/src/assets/audio", path.basename(key)),
|
||||||
|
})),
|
||||||
|
...(await files(ptyRoot))
|
||||||
|
.filter((relative) => !relative.endsWith(".map") && !relative.endsWith(".pdb"))
|
||||||
|
.map((relative) => ({
|
||||||
|
key: `${target.nodePtyPackage}/${relative}`,
|
||||||
|
source: path.join(ptyRoot, relative),
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
await Promise.all(assets.map((asset) => stat(asset.source)))
|
||||||
|
return assets
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hashNodeAssets(assets: readonly NodeAsset[]) {
|
||||||
|
const hash = createHash("sha256")
|
||||||
|
for (const asset of assets.toSorted((left, right) => left.key.localeCompare(right.key))) {
|
||||||
|
hash.update(asset.key)
|
||||||
|
hash.update(await readFile(asset.source))
|
||||||
|
}
|
||||||
|
return hash.digest("hex").slice(0, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyNodeAssets(assets: readonly NodeAsset[]) {
|
||||||
|
const root = path.join(dir, "dist-node", "assets")
|
||||||
|
await Promise.all(
|
||||||
|
assets.map(async (asset) => {
|
||||||
|
const target = path.join(root, asset.key)
|
||||||
|
await mkdir(path.dirname(target), { recursive: true })
|
||||||
|
await copyFile(asset.source, target)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seaAssetMap() {
|
||||||
|
const root = path.join(dir, "dist-node", "assets")
|
||||||
|
return Object.fromEntries((await files(root)).map((key) => [key.replaceAll(path.sep, "/"), path.join(root, key)]))
|
||||||
|
}
|
||||||
@@ -18,22 +18,26 @@ async function publish(dir: string, name: string, version: string) {
|
|||||||
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function publishDistribution(input: { root: string; name: string; binary: string; packagePrefix: string }) {
|
||||||
const binaries: Record<string, string> = {}
|
const binaries: Record<string, string> = {}
|
||||||
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
|
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: input.root })) {
|
||||||
const item = await Bun.file(`./dist/${filepath}`).json()
|
const item = await Bun.file(`${input.root}/${filepath}`).json()
|
||||||
|
if (!item.name.startsWith(input.packagePrefix)) continue
|
||||||
binaries[item.name] = item.version
|
binaries[item.name] = item.version
|
||||||
}
|
}
|
||||||
console.log("binaries", binaries)
|
console.log(input.name, "binaries", binaries)
|
||||||
const version = Object.values(binaries)[0]
|
const versions = new Set(Object.values(binaries))
|
||||||
const name = pkg.name
|
if (versions.size > 1) throw new Error(`Binary package versions do not match for ${input.name}`)
|
||||||
|
const version = versions.values().next().value
|
||||||
|
if (!version) throw new Error(`No binary packages found for ${input.name}`)
|
||||||
|
|
||||||
await $`mkdir -p ./dist/${name}/bin`
|
await $`mkdir -p ${input.root}/${input.name}/bin`
|
||||||
await $`cp ./bin/opencode2.cjs ./dist/${name}/bin/opencode2`
|
await $`cp ./bin/opencode2.cjs ${input.root}/${input.name}/bin/${input.binary}`
|
||||||
await Bun.file(`./dist/${name}/package.json`).write(
|
await Bun.file(`${input.root}/${input.name}/package.json`).write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name,
|
name: input.name,
|
||||||
bin: { opencode2: "./bin/opencode2" },
|
bin: { [input.binary]: `./bin/${input.binary}` },
|
||||||
version,
|
version,
|
||||||
license: pkg.license,
|
license: pkg.license,
|
||||||
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
|
||||||
@@ -48,7 +52,21 @@ await Bun.file(`./dist/${name}/package.json`).write(
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(binaries).map(([name, version]) =>
|
Object.entries(binaries).map(([name, version]) =>
|
||||||
publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
|
publish(`${input.root}/${name.replace("@opencode-ai/", "")}`, name, version),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
await publish(`./dist/${name}`, name, version)
|
await publish(`${input.root}/${input.name}`, input.name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
await publishDistribution({
|
||||||
|
root: "./dist",
|
||||||
|
name: pkg.name,
|
||||||
|
binary: "opencode2",
|
||||||
|
packagePrefix: "@opencode-ai/cli-",
|
||||||
|
})
|
||||||
|
await publishDistribution({
|
||||||
|
root: "./dist/node",
|
||||||
|
name: "opencode2-node",
|
||||||
|
binary: "opencode2-node",
|
||||||
|
packagePrefix: "@opencode-ai/cli-node-",
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import fs from "node:fs/promises"
|
|||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
|
||||||
const target = `cli-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
const nodeBuild = process.argv.includes("--node")
|
||||||
const directory = path.join(import.meta.dir, "..", "dist", target, "bin")
|
const target = `cli${nodeBuild ? "-node" : ""}-${process.platform === "win32" ? "windows" : process.platform}-${process.arch}`
|
||||||
const binary = path.join(directory, `opencode2${process.platform === "win32" ? ".exe" : ""}`)
|
const directory = path.join(import.meta.dir, "..", "dist", ...(nodeBuild ? ["node"] : []), target, "bin")
|
||||||
|
const binary = path.join(directory, `opencode2${nodeBuild ? "-node" : ""}${process.platform === "win32" ? ".exe" : ""}`)
|
||||||
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
if (!(await Bun.file(binary).exists())) throw new Error(`Missing compiled CLI in ${directory}`)
|
||||||
|
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-smoke-"))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { readFile, stat, writeFile } from "node:fs/promises"
|
||||||
import { Effect, Option } from "effect"
|
import { Effect, Option } from "effect"
|
||||||
import { applyEdits, modify } from "jsonc-parser"
|
import { applyEdits, modify } from "jsonc-parser"
|
||||||
import { Global } from "@opencode-ai/core/global"
|
import { Global } from "@opencode-ai/core/global"
|
||||||
@@ -35,7 +36,7 @@ export default Runtime.handler(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function resolveConfigPath(directory: string) {
|
export async function resolveConfigPath(directory: string) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(directory, "opencode.json"),
|
path.join(directory, "opencode.json"),
|
||||||
path.join(directory, "opencode.jsonc"),
|
path.join(directory, "opencode.jsonc"),
|
||||||
@@ -43,16 +44,24 @@ async function resolveConfigPath(directory: string) {
|
|||||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||||
]
|
]
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (await Bun.file(candidate).exists()) return candidate
|
if (
|
||||||
|
await stat(candidate).then(
|
||||||
|
(info) => info.isFile(),
|
||||||
|
() => false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return candidate
|
||||||
}
|
}
|
||||||
return candidates[0]
|
return candidates[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function write(configPath: string, name: string, server: unknown) {
|
async function write(configPath: string, name: string, server: unknown) {
|
||||||
const file = Bun.file(configPath)
|
const text = await readFile(configPath, "utf8").catch((error) => {
|
||||||
const text = (await file.exists()) ? await file.text() : "{}"
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return "{}"
|
||||||
|
throw error
|
||||||
|
})
|
||||||
const edits = modify(text, ["mcp", "servers", name], server, {
|
const edits = modify(text, ["mcp", "servers", name], server, {
|
||||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||||
})
|
})
|
||||||
await Bun.write(configPath, applyEdits(text, edits))
|
await writeFile(configPath, applyEdits(text, edits))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|||||||
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
||||||
import { transparent, type RunFooterTheme } from "./theme"
|
import { transparent, type RunFooterTheme } from "./theme"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
|
|
||||||
export const FOOTER_MENU_ROWS = 8
|
export const FOOTER_MENU_ROWS = 8
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ export function RunFooterMenu(props: {
|
|||||||
...props
|
...props
|
||||||
.items()
|
.items()
|
||||||
.filter((item) => item.description)
|
.filter((item) => item.description)
|
||||||
.map((item) => Bun.stringWidth(item.display)),
|
.map((item) => stringWidth(item.display)),
|
||||||
)
|
)
|
||||||
return width === 0 ? 0 : width + 2
|
return width === 0 ? 0 : width + 2
|
||||||
})
|
})
|
||||||
@@ -205,14 +206,14 @@ export function RunFooterMenu(props: {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return " ".repeat(Math.max(1, descriptionColumn() - Bun.stringWidth(item.display)))
|
return " ".repeat(Math.max(1, descriptionColumn() - stringWidth(item.display)))
|
||||||
}
|
}
|
||||||
const descriptionText = (item: RunFooterMenuItem) => {
|
const descriptionText = (item: RunFooterMenuItem) => {
|
||||||
if (!item.description) {
|
if (!item.description) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const footerWidth = item.footer ? Bun.stringWidth(item.footer) + 1 : 0
|
const footerWidth = item.footer ? stringWidth(item.footer) + 1 : 0
|
||||||
const available =
|
const available =
|
||||||
term().width -
|
term().width -
|
||||||
(border() ? 1 : 0) -
|
(border() ? 1 : 0) -
|
||||||
|
|||||||
@@ -5,14 +5,15 @@
|
|||||||
// It produces a PromptState that RunPromptBody renders as a slim single-line
|
// It produces a PromptState that RunPromptBody renders as a slim single-line
|
||||||
// composer while the footer view renders any active menus below it.
|
// composer while the footer view renders any active menus below it.
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import { pathToFileURL } from "bun"
|
|
||||||
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
import { StyledText, fg, type ColorInput, type KeyEvent, type TextareaRenderable } from "@opentui/core"
|
||||||
import { useRenderer } from "@opentui/solid"
|
import { useRenderer } from "@opentui/solid"
|
||||||
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
|
import { normalizePromptContent } from "@opencode-ai/tui/prompt/content"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
import {
|
import {
|
||||||
createPromptHistory,
|
createPromptHistory,
|
||||||
displayCharAt,
|
displayCharAt,
|
||||||
@@ -602,7 +603,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const restore = (value: RunPrompt, cursor = Bun.stringWidth(value.text)) => {
|
const restore = (value: RunPrompt, cursor = stringWidth(value.text)) => {
|
||||||
draft = clonePrompt(value)
|
draft = clonePrompt(value)
|
||||||
setShell(value.mode === "shell")
|
setShell(value.mode === "shell")
|
||||||
if (!area || area.isDestroyed) {
|
if (!area || area.isDestroyed) {
|
||||||
@@ -612,7 +613,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
hide()
|
hide()
|
||||||
area.setText(value.text)
|
area.setText(value.text)
|
||||||
restoreParts(value.parts)
|
restoreParts(value.parts)
|
||||||
area.cursorOffset = Math.min(cursor, Bun.stringWidth(area.plainText))
|
area.cursorOffset = Math.min(cursor, stringWidth(area.plainText))
|
||||||
scheduleRows()
|
scheduleRows()
|
||||||
area.focus()
|
area.focus()
|
||||||
}
|
}
|
||||||
@@ -643,7 +644,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
area.setText(text)
|
area.setText(text)
|
||||||
clearParts()
|
clearParts()
|
||||||
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
|
draft = shell() ? { text: area.plainText, parts: [], mode: "shell" } : { text: area.plainText, parts: [] }
|
||||||
area.cursorOffset = Math.min(Bun.stringWidth(text), Bun.stringWidth(area.plainText))
|
area.cursorOffset = Math.min(stringWidth(text), stringWidth(area.plainText))
|
||||||
scheduleRows()
|
scheduleRows()
|
||||||
area.focus()
|
area.focus()
|
||||||
}
|
}
|
||||||
@@ -777,7 +778,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
if (move(dir, event)) return
|
if (move(dir, event)) return
|
||||||
if (!area || area.isDestroyed) return false
|
if (!area || area.isDestroyed) return false
|
||||||
|
|
||||||
const endOffset = Bun.stringWidth(area.plainText)
|
const endOffset = stringWidth(area.plainText)
|
||||||
if (dir === -1) {
|
if (dir === -1) {
|
||||||
if (area.cursorOffset === 0) return false
|
if (area.cursorOffset === 0) return false
|
||||||
if (area.visualCursor.visualRow === 0) {
|
if (area.visualCursor.visualRow === 0) {
|
||||||
@@ -886,16 +887,12 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
area.cursorOffset = 0
|
area.cursorOffset = 0
|
||||||
const start = area.logicalCursor
|
const start = area.logicalCursor
|
||||||
area.cursorOffset =
|
area.cursorOffset =
|
||||||
shell() || !head
|
shell() || !head ? cursor : local ? stringWidth(area.plainText) : stringWidth(area.plainText.slice(0, head.end))
|
||||||
? cursor
|
|
||||||
: local
|
|
||||||
? Bun.stringWidth(area.plainText)
|
|
||||||
: Bun.stringWidth(area.plainText.slice(0, head.end))
|
|
||||||
const end = area.logicalCursor
|
const end = area.logicalCursor
|
||||||
|
|
||||||
area.deleteRange(start.row, start.col, end.row, end.col)
|
area.deleteRange(start.row, start.col, end.row, end.col)
|
||||||
area.insertText(text)
|
area.insertText(text)
|
||||||
area.cursorOffset = Bun.stringWidth(text)
|
area.cursorOffset = stringWidth(text)
|
||||||
hide()
|
hide()
|
||||||
syncDraft()
|
syncDraft()
|
||||||
if (!shell()) {
|
if (!shell()) {
|
||||||
@@ -920,7 +917,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||||||
|
|
||||||
const text = "@" + next.value
|
const text = "@" + next.value
|
||||||
const startOffset = at()
|
const startOffset = at()
|
||||||
const endOffset = startOffset + Bun.stringWidth(text)
|
const endOffset = startOffset + stringWidth(text)
|
||||||
const part = structuredClone(next.part)
|
const part = structuredClone(next.part)
|
||||||
if (part.type === "agent") {
|
if (part.type === "agent") {
|
||||||
part.source = {
|
part.source = {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ServerConnection } from "../services/server-connection"
|
|||||||
import { waitForCatalogReady } from "./catalog.shared"
|
import { waitForCatalogReady } from "./catalog.shared"
|
||||||
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
|
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
|
||||||
import type { RunInput, RunTuiConfig } from "./types"
|
import type { RunInput, RunTuiConfig } from "./types"
|
||||||
|
import { readStdin } from "../util/io"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
|
||||||
export type MiniCommandInput = {
|
export type MiniCommandInput = {
|
||||||
server: ServerConnection.Resolved
|
server: ServerConnection.Resolved
|
||||||
@@ -22,7 +24,7 @@ export type MiniCommandInput = {
|
|||||||
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
|
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
|
||||||
export async function runMini(input: MiniCommandInput) {
|
export async function runMini(input: MiniCommandInput) {
|
||||||
validate(input)
|
validate(input)
|
||||||
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt)
|
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt)
|
||||||
const runtimeTask = import("./runtime")
|
const runtimeTask = import("./runtime")
|
||||||
const directory = localDirectory()
|
const directory = localDirectory()
|
||||||
|
|
||||||
@@ -123,7 +125,7 @@ async function validateAgent(sdk: OpenCodeClient, directory: string, name?: stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (agent) return name
|
if (agent) return name
|
||||||
await Bun.sleep(25)
|
await setTimeout(25)
|
||||||
}
|
}
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
warning("failed to list agents. Falling back to default agent")
|
warning("failed to list agents. Falling back to default agent")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
import type { MiniToolPart } from "./types"
|
import type { MiniToolPart } from "./types"
|
||||||
|
|
||||||
@@ -478,11 +479,11 @@ async function prepareFile(file: File) {
|
|||||||
if (file.mime !== "text/plain") {
|
if (file.mime !== "text/plain") {
|
||||||
const uri = file.url.startsWith("data:")
|
const uri = file.url.startsWith("data:")
|
||||||
? file.url
|
? file.url
|
||||||
: `data:${file.mime};base64,${Buffer.from(await Bun.file(new URL(file.url)).arrayBuffer()).toString("base64")}`
|
: `data:${file.mime};base64,${(await readFile(new URL(file.url))).toString("base64")}`
|
||||||
return { attachment: { uri, mime: file.mime, name: file.filename } }
|
return { attachment: { uri, mime: file.mime, name: file.filename } }
|
||||||
}
|
}
|
||||||
const content = file.url.startsWith("data:")
|
const content = file.url.startsWith("data:")
|
||||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||||
: await Bun.file(new URL(file.url)).text()
|
: await readFile(new URL(file.url), "utf8")
|
||||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
// the current draft is saved and history begins. Arrowing past the end
|
// the current draft is saved and history begins. Arrowing past the end
|
||||||
// restores the draft.
|
// restores the draft.
|
||||||
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
|
export { displayCharAt, displaySlice, mentionTriggerIndex } from "@opencode-ai/tui/prompt/display"
|
||||||
|
import { stringWidth } from "@opencode-ai/tui/util/string-width"
|
||||||
import type { RunPrompt } from "./types"
|
import type { RunPrompt } from "./types"
|
||||||
|
|
||||||
const HISTORY_LIMIT = 200
|
const HISTORY_LIMIT = 200
|
||||||
@@ -102,7 +103,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
return { state, apply: false }
|
return { state, apply: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir === 1 && cursor !== Bun.stringWidth(text)) {
|
if (dir === 1 && cursor !== stringWidth(text)) {
|
||||||
return { state, apply: false }
|
return { state, apply: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
index: null,
|
index: null,
|
||||||
},
|
},
|
||||||
text: state.draft,
|
text: state.draft,
|
||||||
cursor: Bun.stringWidth(state.draft),
|
cursor: stringWidth(state.draft),
|
||||||
apply: true,
|
apply: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ export function movePromptHistory(state: PromptHistoryState, dir: -1 | 1, text:
|
|||||||
index: idx,
|
index: idx,
|
||||||
},
|
},
|
||||||
text: state.items[idx].text,
|
text: state.items[idx].text,
|
||||||
cursor: dir === -1 ? 0 : Bun.stringWidth(state.items[idx].text),
|
cursor: dir === -1 ? 0 : stringWidth(state.items[idx].text),
|
||||||
apply: true,
|
apply: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
|
|||||||
import { Model } from "@opencode-ai/schema/model"
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
import { open } from "node:fs/promises"
|
import { open } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { readStdin } from "../util/io"
|
||||||
import { ServerConnection } from "../services/server-connection"
|
import { ServerConnection } from "../services/server-connection"
|
||||||
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||||
import { runNonInteractivePrompt } from "./noninteractive"
|
import { runNonInteractivePrompt } from "./noninteractive"
|
||||||
@@ -48,7 +49,7 @@ async function run(input: RunCommandInput) {
|
|||||||
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")
|
||||||
const root = process.env.PWD ?? process.cwd()
|
const root = process.env.PWD ?? process.cwd()
|
||||||
const directory = localDirectory(root)
|
const directory = localDirectory(root)
|
||||||
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await Bun.stdin.text())
|
const message = mergeInput(formatMessage(input.message), process.stdin.isTTY ? undefined : await readStdin())
|
||||||
if (!message?.trim()) fail("You must provide a message")
|
if (!message?.trim()) fail("You must provide a message")
|
||||||
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
|
const files = await Promise.all(input.file.map((file) => prepareFile(file, root)))
|
||||||
const prepared = { directory, message, files }
|
const prepared = { directory, message, files }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { readFile } from "node:fs/promises"
|
||||||
import type {
|
import type {
|
||||||
EventSubscribeOutput,
|
EventSubscribeOutput,
|
||||||
OpenCodeClient,
|
OpenCodeClient,
|
||||||
@@ -161,7 +162,7 @@ async function prepareFile(file: RunFilePart) {
|
|||||||
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
|
if (file.mime !== "text/plain") return { attachment: { uri: file.url, name: file.filename } }
|
||||||
const content = file.url.startsWith("data:")
|
const content = file.url.startsWith("data:")
|
||||||
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
? Buffer.from(file.url.slice(file.url.indexOf(",") + 1), "base64").toString("utf8")
|
||||||
: await Bun.file(new URL(file.url)).text()
|
: await readFile(new URL(file.url), "utf8")
|
||||||
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
return { text: `<file name="${file.filename}">\n${content}\n</file>` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import "./plugin-runtime.promise"
|
||||||
|
import "./plugin-runtime.effect"
|
||||||
|
import "../index"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
Agent,
|
||||||
|
Command,
|
||||||
|
Connection,
|
||||||
|
Credential,
|
||||||
|
Integration,
|
||||||
|
Model,
|
||||||
|
Plugin,
|
||||||
|
Provider,
|
||||||
|
Reference,
|
||||||
|
Skill,
|
||||||
|
} from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
|
||||||
|
|
||||||
|
const key = Symbol.for("opencode.plugin.v2.effect")
|
||||||
|
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||||
|
Agent,
|
||||||
|
Command,
|
||||||
|
Connection,
|
||||||
|
Credential,
|
||||||
|
Integration,
|
||||||
|
Model,
|
||||||
|
Plugin,
|
||||||
|
Provider,
|
||||||
|
Reference,
|
||||||
|
Skill,
|
||||||
|
Tool,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Agent,
|
||||||
|
Command,
|
||||||
|
Connection,
|
||||||
|
Credential,
|
||||||
|
Integration,
|
||||||
|
Model,
|
||||||
|
Plugin,
|
||||||
|
Provider,
|
||||||
|
Reference,
|
||||||
|
Skill,
|
||||||
|
} from "@opencode-ai/plugin/v2"
|
||||||
|
|
||||||
|
const key = Symbol.for("opencode.plugin.v2.promise")
|
||||||
|
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||||
|
Agent,
|
||||||
|
Command,
|
||||||
|
Connection,
|
||||||
|
Credential,
|
||||||
|
Integration,
|
||||||
|
Model,
|
||||||
|
Plugin,
|
||||||
|
Provider,
|
||||||
|
Reference,
|
||||||
|
Skill,
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
const platforms = ["darwin", "linux", "win32"] as const
|
||||||
|
|
||||||
|
export type NodeTarget = ReturnType<typeof nodeTarget>
|
||||||
|
|
||||||
|
export function nodeTarget(platform: string, arch: string) {
|
||||||
|
if (!platforms.includes(platform as (typeof platforms)[number]) || (arch !== "arm64" && arch !== "x64")) {
|
||||||
|
throw new Error(`Unsupported Node executable target: ${platform}-${arch}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlatform = platform as (typeof platforms)[number]
|
||||||
|
const targetArch = arch as "arm64" | "x64"
|
||||||
|
const nodePtyPackage = `@lydell/node-pty-${targetPlatform}-${targetArch}`
|
||||||
|
const parcelWatcherPackage = `@parcel/watcher-${targetPlatform}-${targetArch}${targetPlatform === "linux" ? "-glibc" : ""}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform: targetPlatform,
|
||||||
|
arch: targetArch,
|
||||||
|
nodePtyPackage,
|
||||||
|
nodePtyEntryAsset: `${nodePtyPackage}/lib/index.js`,
|
||||||
|
parcelWatcherPackage,
|
||||||
|
parcelWatcherAsset: `${parcelWatcherPackage}/watcher.node`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const photonWasmAsset = "@silvia-odwyer/photon-node/photon_rs_bg.wasm"
|
||||||
|
export const nodeExecArgv = ["--experimental-ffi", "--use-system-ca", "--disable-warning=ExperimentalWarning"] as const
|
||||||
|
|
||||||
|
export const attentionSoundAssets = [
|
||||||
|
"@opencode-ai/ui/audio/bip-bop-01.mp3",
|
||||||
|
"@opencode-ai/ui/audio/bip-bop-03.mp3",
|
||||||
|
"@opencode-ai/ui/audio/staplebops-06.mp3",
|
||||||
|
"@opencode-ai/ui/audio/nope-03.mp3",
|
||||||
|
"@opencode-ai/ui/audio/yup-01.mp3",
|
||||||
|
] as const
|
||||||
@@ -5,6 +5,7 @@ import { Service } from "@opencode-ai/client/effect/service"
|
|||||||
import { Effect, FileSystem, Option, Schema } from "effect"
|
import { Effect, FileSystem, Option, Schema } from "effect"
|
||||||
import { randomBytes } from "crypto"
|
import { randomBytes } from "crypto"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { selfCommand } from "../util/process"
|
||||||
|
|
||||||
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
// The CLI's service configuration file, plus the Service.EnsureOptions binding that
|
||||||
// points the client package's service operations at this CLI: which
|
// points the client package's service operations at this CLI: which
|
||||||
@@ -78,13 +79,10 @@ const paths = Effect.gen(function* () {
|
|||||||
export const options = Effect.fnUntraced(function* () {
|
export const options = Effect.fnUntraced(function* () {
|
||||||
const { file, legacyFile } = yield* paths
|
const { file, legacyFile } = yield* paths
|
||||||
yield* migrateRegistration(legacyFile, file)
|
yield* migrateRegistration(legacyFile, file)
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
|
||||||
const entrypoint = compiled ? undefined : process.argv[1]
|
|
||||||
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
|
||||||
return {
|
return {
|
||||||
file,
|
file,
|
||||||
version: InstallationVersion,
|
version: InstallationVersion,
|
||||||
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
|
command: [...selfCommand(), "serve", "--service"],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||||||
import { Effect, Schema, Stream } from "effect"
|
import { Effect, Schema, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { randomBytes } from "node:crypto"
|
import { randomBytes } from "node:crypto"
|
||||||
import path from "node:path"
|
import { selfCommand } from "../util/process"
|
||||||
|
|
||||||
const Ready = Schema.Struct({ url: Schema.String })
|
const Ready = Schema.Struct({ url: Schema.String })
|
||||||
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
const decodeReady = Schema.decodeUnknownPromise(Schema.fromJsonString(Ready))
|
||||||
@@ -14,10 +14,7 @@ type Options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function command(password: string, options: Options) {
|
function command(password: string, options: Options) {
|
||||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
const [executable, ...args] = options.command ?? [...selfCommand(), "serve"]
|
||||||
const entrypoint = compiled ? [] : process.argv[1] ? [process.argv[1]] : []
|
|
||||||
if (!compiled && entrypoint.length === 0) throw new Error("Failed to resolve CLI entrypoint")
|
|
||||||
const [executable, ...args] = options.command ?? [process.execPath, ...entrypoint, "serve"]
|
|
||||||
if (!executable) throw new Error("Failed to resolve standalone server command")
|
if (!executable) throw new Error("Failed to resolve standalone server command")
|
||||||
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
|
return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|||||||
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
import { registerOpencodeSpinner } from "@opencode-ai/tui/component/register-spinner"
|
||||||
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
import { SPINNER_FRAMES } from "@opencode-ai/tui/component/spinner"
|
||||||
import { go } from "@opencode-ai/tui/logo"
|
import { go } from "@opencode-ai/tui/logo"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
import {
|
import {
|
||||||
batch,
|
batch,
|
||||||
createEffect,
|
createEffect,
|
||||||
@@ -123,7 +124,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
let shownAt = performance.now()
|
let shownAt = performance.now()
|
||||||
const waitForStage = async () => {
|
const waitForStage = async () => {
|
||||||
const remaining = stageFloor - (performance.now() - shownAt)
|
const remaining = stageFloor - (performance.now() - shownAt)
|
||||||
if (remaining > 0) await Bun.sleep(remaining)
|
if (remaining > 0) await setTimeout(remaining)
|
||||||
}
|
}
|
||||||
const advance = async (stage: number) => {
|
const advance = async (stage: number) => {
|
||||||
await waitForStage()
|
await waitForStage()
|
||||||
@@ -140,11 +141,11 @@ async function open(from?: string): Promise<Session> {
|
|||||||
setOutcome(next)
|
setOutcome(next)
|
||||||
const completed = await Promise.race([
|
const completed = await Promise.race([
|
||||||
settled.promise.then(() => true),
|
settled.promise.then(() => true),
|
||||||
Bun.sleep(transitionDuration + 500).then(() => false),
|
setTimeout(transitionDuration + 500).then(() => false),
|
||||||
])
|
])
|
||||||
resolveOutcome = undefined
|
resolveOutcome = undefined
|
||||||
setAnimating(false)
|
setAnimating(false)
|
||||||
if (completed) await Bun.sleep(hold)
|
if (completed) await setTimeout(hold)
|
||||||
}
|
}
|
||||||
let closing: Promise<void> | undefined
|
let closing: Promise<void> | undefined
|
||||||
let transferred = false
|
let transferred = false
|
||||||
@@ -154,7 +155,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
setAnimating(false)
|
setAnimating(false)
|
||||||
if (renderer.isDestroyed) return
|
if (renderer.isDestroyed) return
|
||||||
renderer.pause()
|
renderer.pause()
|
||||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||||
renderer.destroy()
|
renderer.destroy()
|
||||||
})())
|
})())
|
||||||
let loading: Promise<void> | undefined
|
let loading: Promise<void> | undefined
|
||||||
@@ -178,7 +179,7 @@ async function open(from?: string): Promise<Session> {
|
|||||||
renderer.screenMode = "alternate-screen"
|
renderer.screenMode = "alternate-screen"
|
||||||
renderer.consoleMode = "console-overlay"
|
renderer.consoleMode = "console-overlay"
|
||||||
renderer.requestRender()
|
renderer.requestRender()
|
||||||
await Promise.race([renderer.idle(), Bun.sleep(500)])
|
await Promise.race([renderer.idle(), setTimeout(500)])
|
||||||
transferred = true
|
transferred = true
|
||||||
return {
|
return {
|
||||||
renderer,
|
renderer,
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import { parse, type ParseError } from "jsonc-parser"
|
|||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import semver from "semver"
|
import semver from "semver"
|
||||||
|
|
||||||
|
declare const OPENCODE_CLI_NAME: string | undefined
|
||||||
|
|
||||||
export type Policy = boolean | "notify"
|
export type Policy = boolean | "notify"
|
||||||
export type Action = "none" | "upgrade"
|
export type Action = "none" | "upgrade"
|
||||||
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
type Method = "npm" | "pnpm" | "bun" | "yarn"
|
||||||
|
|
||||||
const packageName = "@opencode-ai/cli"
|
const packageName =
|
||||||
|
typeof OPENCODE_CLI_NAME === "string" && OPENCODE_CLI_NAME === "opencode2-node"
|
||||||
|
? OPENCODE_CLI_NAME
|
||||||
|
: "@opencode-ai/cli"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly check: () => Effect.Effect<void>
|
readonly check: () => Effect.Effect<void>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { text } from "node:stream/consumers"
|
||||||
|
|
||||||
|
export function readStdin() {
|
||||||
|
return text(process.stdin)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
export function selfCommand() {
|
||||||
|
const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase()
|
||||||
|
if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath]
|
||||||
|
if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint")
|
||||||
|
if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]]
|
||||||
|
return [process.execPath, process.argv[1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeFlags() {
|
||||||
|
return process.execArgv.flatMap((arg, index, args) => {
|
||||||
|
if (index > 0 && args[index - 1] === "--conditions") return []
|
||||||
|
if (arg === "--conditions") return args[index + 1] ? [arg, args[index + 1]] : []
|
||||||
|
if (arg.startsWith("--conditions=")) return [arg]
|
||||||
|
if (
|
||||||
|
arg === "--experimental-ffi" ||
|
||||||
|
arg === "--use-system-ca" ||
|
||||||
|
arg === "--enable-source-maps" ||
|
||||||
|
arg === "--no-addons"
|
||||||
|
)
|
||||||
|
return [arg]
|
||||||
|
if (arg === "--no-warnings" || arg.startsWith("--disable-warning=")) return [arg]
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
"jsxImportSource": "@opentui/solid",
|
"jsxImportSource": "@opentui/solid",
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"noUncheckedIndexedAccess": false
|
"noUncheckedIndexedAccess": false
|
||||||
}
|
},
|
||||||
|
"exclude": ["dist", "dist-node"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
import { createRequire } from "node:module"
|
||||||
|
import { defineConfig, type Plugin, type UserConfig } from "vite"
|
||||||
|
import solid from "vite-plugin-solid"
|
||||||
|
import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset } from "./src/node/target"
|
||||||
|
|
||||||
|
const dir = import.meta.dirname
|
||||||
|
|
||||||
|
function rawTextPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: "opencode:raw-text",
|
||||||
|
async load(id) {
|
||||||
|
if (!id.endsWith(".md")) return
|
||||||
|
return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeRequirePlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: "opencode:runtime-require",
|
||||||
|
enforce: "pre",
|
||||||
|
transform(code, id) {
|
||||||
|
if (!id.endsWith("turndown/lib/turndown.es.js")) return
|
||||||
|
const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "")
|
||||||
|
if (transformed === code) this.error("Failed to rewrite Turndown's Domino require")
|
||||||
|
return `import domino from "@mixmark-io/domino"\n${transformed}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolve = {
|
||||||
|
alias: [
|
||||||
|
{ find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
|
||||||
|
{ find: /^solid-js$/, replacement: "solid-js/dist/solid.js" },
|
||||||
|
{
|
||||||
|
find: /^ws$/,
|
||||||
|
replacement: path.join(path.dirname(createRequire(import.meta.url).resolve("ws/package.json")), "wrapper.mjs"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
conditions: ["node"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = (entryFileNames: string, banner?: string) => ({
|
||||||
|
format: "esm" as const,
|
||||||
|
entryFileNames,
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
banner,
|
||||||
|
})
|
||||||
|
|
||||||
|
function nodePrelude(input: NodeBuildInput) {
|
||||||
|
const nodePtySpawnHelper =
|
||||||
|
input.target.platform === "darwin"
|
||||||
|
? `${input.target.nodePtyPackage}/prebuilds/darwin-${input.target.arch}/spawn-helper`
|
||||||
|
: undefined
|
||||||
|
const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||||
|
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||||
|
export const Agent = sdk.Agent
|
||||||
|
export const Command = sdk.Command
|
||||||
|
export const Connection = sdk.Connection
|
||||||
|
export const Credential = sdk.Credential
|
||||||
|
export const Integration = sdk.Integration
|
||||||
|
export const Model = sdk.Model
|
||||||
|
export const Plugin = sdk.Plugin
|
||||||
|
export const Provider = sdk.Provider
|
||||||
|
export const Reference = sdk.Reference
|
||||||
|
export const Skill = sdk.Skill`
|
||||||
|
const effectModule = promiseModule
|
||||||
|
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||||
|
.replace("Promise plugin", "Effect plugin")
|
||||||
|
const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||||
|
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||||
|
export const define = sdk.Plugin.define`
|
||||||
|
const effectPluginModule = promisePluginModule
|
||||||
|
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||||
|
.replace("Promise plugin", "Effect plugin")
|
||||||
|
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
|
||||||
|
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
|
||||||
|
export const Tool = sdk.Tool
|
||||||
|
export const Failure = sdk.Tool.Failure
|
||||||
|
export const RegistrationError = sdk.Tool.RegistrationError
|
||||||
|
export const make = sdk.Tool.make
|
||||||
|
export const validateName = sdk.Tool.validateName
|
||||||
|
export const registrationEntries = sdk.Tool.registrationEntries
|
||||||
|
export const withPermission = sdk.Tool.withPermission
|
||||||
|
export const permission = sdk.Tool.permission
|
||||||
|
export const definition = sdk.Tool.definition
|
||||||
|
export const settle = sdk.Tool.settle`
|
||||||
|
return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")}
|
||||||
|
import __cjs_mod__ from "node:module"
|
||||||
|
import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs"
|
||||||
|
import { tmpdir as __ocTmpdir } from "node:os"
|
||||||
|
import __ocPath from "node:path"
|
||||||
|
import { getAssetKeys as __ocAssetKeys, getRawAsset as __ocRawAsset, isSea as __ocIsSea } from "node:sea"
|
||||||
|
import { fileURLToPath as __ocFileURLToPath } from "node:url"
|
||||||
|
const __filename = import.meta.filename
|
||||||
|
const __dirname = import.meta.dirname
|
||||||
|
const require = __cjs_mod__.createRequire(import.meta.url)
|
||||||
|
const __ocPluginModules = ${JSON.stringify({
|
||||||
|
"@opencode-ai/plugin/v2": "opencode:plugin-v2",
|
||||||
|
"@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin",
|
||||||
|
"@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect",
|
||||||
|
"@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin",
|
||||||
|
"@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool",
|
||||||
|
})}
|
||||||
|
const __ocPluginSources = ${JSON.stringify({
|
||||||
|
"opencode:plugin-v2": promiseModule,
|
||||||
|
"opencode:plugin-v2-plugin": promisePluginModule,
|
||||||
|
"opencode:plugin-v2-effect": effectModule,
|
||||||
|
"opencode:plugin-v2-effect-plugin": effectPluginModule,
|
||||||
|
"opencode:plugin-v2-effect-tool": effectToolModule,
|
||||||
|
})}
|
||||||
|
__cjs_mod__.registerHooks({
|
||||||
|
resolve(__ocSpecifier, __ocContext, __ocNextResolve) {
|
||||||
|
const __ocUrl = __ocPluginModules[__ocSpecifier]
|
||||||
|
return __ocUrl ? { url: __ocUrl, shortCircuit: true } : __ocNextResolve(__ocSpecifier, __ocContext)
|
||||||
|
},
|
||||||
|
load(__ocUrl, __ocContext, __ocNextLoad) {
|
||||||
|
const __ocSource = __ocPluginSources[__ocUrl]
|
||||||
|
return __ocSource
|
||||||
|
? { format: "module", source: __ocSource, shortCircuit: true }
|
||||||
|
: __ocNextLoad(__ocUrl, __ocContext)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const __ocUid = typeof process.getuid === "function" ? process.getuid() : undefined
|
||||||
|
const __ocCacheRoot = __ocPath.join(__ocTmpdir(), \`opencode-node-\${__ocUid ?? "user"}\`)
|
||||||
|
if (__ocIsSea()) {
|
||||||
|
try {
|
||||||
|
__ocMkdir(__ocCacheRoot, { mode: 0o700 })
|
||||||
|
} catch (__ocError) {
|
||||||
|
if (!__ocExists(__ocCacheRoot)) throw __ocError
|
||||||
|
}
|
||||||
|
const __ocCacheInfo = __ocLstat(__ocCacheRoot)
|
||||||
|
if (!__ocCacheInfo.isDirectory() || __ocCacheInfo.isSymbolicLink()) throw new Error("Unsafe Node asset cache path")
|
||||||
|
if (__ocUid !== undefined && __ocCacheInfo.uid !== __ocUid) throw new Error("Node asset cache is owned by another user")
|
||||||
|
if (__ocUid !== undefined) __ocChmod(__ocCacheRoot, 0o700)
|
||||||
|
}
|
||||||
|
const __ocAssetRoot = __ocIsSea()
|
||||||
|
? __ocPath.join(__ocCacheRoot, ${JSON.stringify(`${input.assetHash}-${input.target.platform}-${input.target.arch}`)})
|
||||||
|
: __ocFileURLToPath(new URL("./assets/", import.meta.url))
|
||||||
|
if (__ocIsSea()) {
|
||||||
|
for (const __ocKey of __ocAssetKeys()) {
|
||||||
|
const __ocTarget = __ocPath.join(__ocAssetRoot, __ocKey)
|
||||||
|
if (__ocExists(__ocTarget)) continue
|
||||||
|
__ocMkdir(__ocPath.dirname(__ocTarget), { recursive: true })
|
||||||
|
const __ocTemporary = \`${"${__ocTarget}"}.${"${process.pid}"}.${"${crypto.randomUUID()}"}.tmp\`
|
||||||
|
__ocWrite(__ocTemporary, new Uint8Array(__ocRawAsset(__ocKey)))
|
||||||
|
try {
|
||||||
|
__ocRename(__ocTemporary, __ocTarget)
|
||||||
|
} catch (__ocError) {
|
||||||
|
__ocRm(__ocTemporary, { force: true })
|
||||||
|
if (!__ocExists(__ocTarget)) throw __ocError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const __ocPtySpawnHelper = ${JSON.stringify(nodePtySpawnHelper)}
|
||||||
|
if (__ocPtySpawnHelper) __ocChmod(__ocPath.join(__ocAssetRoot, __ocPtySpawnHelper), 0o755)
|
||||||
|
}
|
||||||
|
process.env.OPENCODE_NODE_ASSETS_DIR = __ocAssetRoot
|
||||||
|
process.env.OTUI_ASSET_ROOT = __ocAssetRoot
|
||||||
|
process.env.OPENCODE_NODE_PTY_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.nodePtyEntryAsset)})
|
||||||
|
process.env.OPENCODE_PARCEL_WATCHER_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(input.target.parcelWatcherAsset)})
|
||||||
|
process.env.OPENCODE_PHOTON_WASM_PATH = __ocPath.join(__ocAssetRoot, ${JSON.stringify(photonWasmAsset)})
|
||||||
|
globalThis.__OPENCODE_PHOTON_WASM_PATH = process.env.OPENCODE_PHOTON_WASM_PATH
|
||||||
|
if (process.platform === "linux") process.env.OPENTUI_LIBC = "glibc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeBuildInput = {
|
||||||
|
readonly version: string
|
||||||
|
readonly channel: string
|
||||||
|
readonly models: string
|
||||||
|
readonly assetHash: string
|
||||||
|
readonly target: NodeTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mainConfig(input: NodeBuildInput): UserConfig {
|
||||||
|
return defineConfig({
|
||||||
|
root: dir,
|
||||||
|
plugins: [
|
||||||
|
rawTextPlugin(),
|
||||||
|
runtimeRequirePlugin(),
|
||||||
|
solid({
|
||||||
|
solid: {
|
||||||
|
generate: "universal",
|
||||||
|
moduleName: "@opentui/solid",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve,
|
||||||
|
esbuild: { jsx: "automatic" },
|
||||||
|
define: {
|
||||||
|
OPENCODE_VERSION: JSON.stringify(input.version),
|
||||||
|
OPENCODE_CLI_NAME: JSON.stringify("opencode2-node"),
|
||||||
|
OPENCODE_MODELS_DEV: input.models,
|
||||||
|
OPENCODE_CHANNEL: JSON.stringify(input.channel),
|
||||||
|
OPENCODE_LIBC: input.target.platform === "linux" ? JSON.stringify("glibc") : "undefined",
|
||||||
|
FFF_LIBC: input.target.platform === "linux" ? JSON.stringify("gnu") : "undefined",
|
||||||
|
},
|
||||||
|
ssr: { noExternal: true },
|
||||||
|
build: {
|
||||||
|
ssr: "src/node/index.ts",
|
||||||
|
target: "node26",
|
||||||
|
outDir: "dist-node",
|
||||||
|
emptyOutDir: false,
|
||||||
|
minify: true,
|
||||||
|
rollupOptions: {
|
||||||
|
external: [/^@opencode-ai\/simulation(?:\/|$)/],
|
||||||
|
output: output("opencode.mjs", nodePrelude(input)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mainConfig({
|
||||||
|
version: process.env.OPENCODE_VERSION ?? "local",
|
||||||
|
channel: process.env.OPENCODE_CHANNEL ?? "local",
|
||||||
|
models: "undefined",
|
||||||
|
assetHash: "local",
|
||||||
|
target: nodeTarget(process.platform, process.arch),
|
||||||
|
})
|
||||||
@@ -37,6 +37,21 @@
|
|||||||
"bun": "./src/filesystem/fff.bun.ts",
|
"bun": "./src/filesystem/fff.bun.ts",
|
||||||
"node": "./src/filesystem/fff.node.ts",
|
"node": "./src/filesystem/fff.node.ts",
|
||||||
"default": "./src/filesystem/fff.bun.ts"
|
"default": "./src/filesystem/fff.bun.ts"
|
||||||
|
},
|
||||||
|
"#photon-wasm": {
|
||||||
|
"bun": "./src/image/photon-wasm.bun.ts",
|
||||||
|
"node": "./src/image/photon-wasm.node.ts",
|
||||||
|
"default": "./src/image/photon-wasm.bun.ts"
|
||||||
|
},
|
||||||
|
"#runtime-import": {
|
||||||
|
"bun": "./src/runtime/import.bun.ts",
|
||||||
|
"node": "./src/runtime/import.node.ts",
|
||||||
|
"default": "./src/runtime/import.bun.ts"
|
||||||
|
},
|
||||||
|
"#process-lock-ffi": {
|
||||||
|
"bun": "./src/util/process-lock-ffi.bun.ts",
|
||||||
|
"node": "./src/util/process-lock-ffi.node.ts",
|
||||||
|
"default": "./src/util/process-lock-ffi.bun.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -121,6 +136,7 @@
|
|||||||
"mime-types": "3.0.2",
|
"mime-types": "3.0.2",
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
|
"resolve.exports": "catalog:",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"venice-ai-sdk-provider": "2.1.1",
|
"venice-ai-sdk-provider": "2.1.1",
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import { Flag } from "../flag/flag"
|
|||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { watch as watchFileSystem } from "node:fs"
|
import { watch as watchFileSystem } from "node:fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import { createRequire } from "node:module"
|
||||||
|
|
||||||
declare const OPENCODE_LIBC: string | undefined
|
declare const OPENCODE_LIBC: string | undefined
|
||||||
|
|
||||||
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
export const Event = { Updated: FileSystem.Event.Changed }
|
export const Event = { Updated: FileSystem.Event.Changed }
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
|
|||||||
try {
|
try {
|
||||||
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
|
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
|
||||||
const binding = require(
|
const binding = require(
|
||||||
|
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
|
||||||
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
|
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`,
|
||||||
)
|
)
|
||||||
return createWrapper(binding) as typeof import("@parcel/watcher")
|
return createWrapper(binding) as typeof import("@parcel/watcher")
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// @ts-ignore Bun embeds static file imports when compiling the CLI.
|
||||||
|
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
|
||||||
|
|
||||||
|
export default photonWasm
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createRequire } from "node:module"
|
||||||
|
|
||||||
|
export default process.env.OPENCODE_PHOTON_WASM_PATH ??
|
||||||
|
createRequire(import.meta.url).resolve("@silvia-odwyer/photon-node/photon_rs_bg.wasm")
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-ignore Bun's static file import is embedded by `bun build --compile`; some consumers also declare *.wasm.
|
import photonWasm from "#photon-wasm"
|
||||||
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { fileURLToPath } from "node:url"
|
import { fileURLToPath } from "node:url"
|
||||||
|
|||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
declare module "node:ffi" {
|
||||||
|
type Signature = {
|
||||||
|
readonly arguments?: readonly string[]
|
||||||
|
readonly return?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForeignFunction = (...args: ReadonlyArray<unknown>) => number | bigint
|
||||||
|
|
||||||
|
export function dlopen(
|
||||||
|
path: string,
|
||||||
|
definitions: Readonly<Record<string, Signature>>,
|
||||||
|
): {
|
||||||
|
readonly lib: { close(): void }
|
||||||
|
readonly functions: Readonly<Record<string, ForeignFunction>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInt32(pointer: number | bigint, offset?: number): number
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { filesystem } from "./effect/app-node-platform"
|
|||||||
import { LayerNode } from "./effect/layer-node"
|
import { LayerNode } from "./effect/layer-node"
|
||||||
import { makeRuntime } from "./effect/runtime"
|
import { makeRuntime } from "./effect/runtime"
|
||||||
import { NpmConfig } from "./npm-config"
|
import { NpmConfig } from "./npm-config"
|
||||||
|
import { resolveModule } from "#runtime-import"
|
||||||
|
|
||||||
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedError>()("NpmInstallFailedError", {
|
||||||
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
add: Schema.Array(Schema.String).pipe(Schema.optional),
|
||||||
@@ -54,9 +55,7 @@ const resolveEntryPoint = (name: string, dir: string, subpaths: readonly string[
|
|||||||
const entrypoint = subpaths
|
const entrypoint = subpaths
|
||||||
.map((subpath) => {
|
.map((subpath) => {
|
||||||
try {
|
try {
|
||||||
return typeof Bun !== "undefined"
|
return resolveModule([name, subpath].filter(Boolean).join("/"), dir)
|
||||||
? import.meta.resolve([name, subpath].filter(Boolean).join("/"), dir)
|
|
||||||
: import.meta.resolve(dir)
|
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Effect } from "effect"
|
|||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Npm } from "../../npm"
|
import { Npm } from "../../npm"
|
||||||
|
import { importModule } from "#runtime-import"
|
||||||
|
|
||||||
export const DynamicProviderPlugin = define({
|
export const DynamicProviderPlugin = define({
|
||||||
id: "opencode.provider.dynamic",
|
id: "opencode.provider.dynamic",
|
||||||
@@ -17,11 +18,9 @@ export const DynamicProviderPlugin = define({
|
|||||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||||
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
|
||||||
|
|
||||||
const mod = yield* Effect.promise(async () => {
|
const mod = (yield* Effect.promise(() =>
|
||||||
return (await import(
|
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
||||||
installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href
|
).pipe(Effect.orDie)) as Record<string, (options: any) => any>
|
||||||
)) as Record<string, (options: any) => any>
|
|
||||||
}).pipe(Effect.orDie)
|
|
||||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||||
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
if (!match) throw new Error(`Package ${evt.package} has no provider factory export`)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { pathToFileURL } from "url"
|
|||||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||||
import { Npm } from "../../npm"
|
import { Npm } from "../../npm"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
|
import { importModule } from "#runtime-import"
|
||||||
|
|
||||||
export const SapAICorePlugin = define({
|
export const SapAICorePlugin = define({
|
||||||
id: "opencode.provider.sap-ai-core",
|
id: "opencode.provider.sap-ai-core",
|
||||||
@@ -22,9 +23,9 @@ export const SapAICorePlugin = define({
|
|||||||
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
: (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
|
||||||
if (!installedPath) return yield* Effect.die(new Error(`Package ${evt.package} has no import entrypoint`))
|
if (!installedPath) return yield* Effect.die(new Error(`Package ${evt.package} has no import entrypoint`))
|
||||||
|
|
||||||
const mod: Record<string, unknown> = yield* Effect.promise(
|
const mod = (yield* Effect.promise(() =>
|
||||||
() => import(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
importModule(installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href),
|
||||||
)
|
)) as Record<string, unknown>
|
||||||
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
const match = Object.keys(mod).find((name) => name.startsWith("create"))
|
||||||
if (!match) return yield* Effect.die(new Error(`Package ${evt.package} has no provider factory export`))
|
if (!match) return yield* Effect.die(new Error(`Package ${evt.package} has no provider factory export`))
|
||||||
const factory = mod[match]
|
const factory = mod[match]
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { WellKnown } from "../wellknown"
|
|||||||
import { PluginInternal } from "./internal"
|
import { PluginInternal } from "./internal"
|
||||||
import { PluginRuntime } from "./runtime"
|
import { PluginRuntime } from "./runtime"
|
||||||
import { SdkPlugins } from "./sdk"
|
import { SdkPlugins } from "./sdk"
|
||||||
|
import { importModule } from "#runtime-import"
|
||||||
|
|
||||||
const PluginModule = Schema.Struct({
|
const PluginModule = Schema.Struct({
|
||||||
default: Schema.Union([
|
default: Schema.Union([
|
||||||
@@ -164,9 +165,13 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
|
|||||||
if (!entrypoint) return
|
if (!entrypoint) return
|
||||||
// Bun currently ignores query parameters when caching file:// imports.
|
// Bun currently ignores query parameters when caching file:// imports.
|
||||||
const source =
|
const source =
|
||||||
operation.mtime === undefined ? entrypoint : `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
operation.mtime === undefined
|
||||||
|
? entrypoint
|
||||||
|
: typeof Bun !== "undefined"
|
||||||
|
? `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
|
||||||
|
: `${entrypoint}?mtime=${operation.mtime}`
|
||||||
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
|
yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
|
||||||
const mod = yield* Effect.promise(() => import(source))
|
const mod = yield* Effect.promise(() => importModule(source))
|
||||||
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
|
||||||
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export * as ProviderV2 from "./provider"
|
export * as ProviderV2 from "./provider"
|
||||||
|
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { pathToFileURL } from "url"
|
|
||||||
import { Provider } from "@opencode-ai/schema/provider"
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
|
import type { ProviderPackageDefinition } from "@opencode-ai/ai"
|
||||||
import { Npm } from "./npm"
|
import { Npm } from "./npm"
|
||||||
import type { DeepMutable } from "./schema"
|
import type { DeepMutable } from "./schema"
|
||||||
|
import { importModule, resolveModule } from "#runtime-import"
|
||||||
|
|
||||||
export const ID = Provider.ID
|
export const ID = Provider.ID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
@@ -32,8 +32,24 @@ export class LoadError extends Schema.TaggedErrorClass<LoadError>()("ProviderV2.
|
|||||||
export type ProviderPackage = ProviderPackageDefinition
|
export type ProviderPackage = ProviderPackageDefinition
|
||||||
|
|
||||||
const packages = new Map<string, Promise<unknown>>()
|
const packages = new Map<string, Promise<unknown>>()
|
||||||
|
const builtins = new Map<string, () => Promise<unknown>>([
|
||||||
|
["@opencode-ai/ai/providers/amazon-bedrock", () => import("@opencode-ai/ai/providers/amazon-bedrock")],
|
||||||
|
["@opencode-ai/ai/providers/anthropic", () => import("@opencode-ai/ai/providers/anthropic")],
|
||||||
|
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
|
||||||
|
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],
|
||||||
|
["@opencode-ai/ai/providers/azure/responses", () => import("@opencode-ai/ai/providers/azure/responses")],
|
||||||
|
["@opencode-ai/ai/providers/google", () => import("@opencode-ai/ai/providers/google")],
|
||||||
|
["@opencode-ai/ai/providers/openai", () => import("@opencode-ai/ai/providers/openai")],
|
||||||
|
["@opencode-ai/ai/providers/openai/chat", () => import("@opencode-ai/ai/providers/openai/chat")],
|
||||||
|
["@opencode-ai/ai/providers/openai/responses", () => import("@opencode-ai/ai/providers/openai/responses")],
|
||||||
|
["@opencode-ai/ai/providers/openai-compatible", () => import("@opencode-ai/ai/providers/openai-compatible")],
|
||||||
|
["@opencode-ai/ai/providers/openrouter", () => import("@opencode-ai/ai/providers/openrouter")],
|
||||||
|
["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")],
|
||||||
|
])
|
||||||
|
|
||||||
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
|
export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) {
|
||||||
|
const builtin = builtins.get(specifier)
|
||||||
|
if (builtin) return yield* importPackage(specifier, specifier, builtin)
|
||||||
const resolved = yield* Effect.sync(() => {
|
const resolved = yield* Effect.sync(() => {
|
||||||
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/ai/")) return specifier
|
if (specifier.startsWith("file://") || specifier.startsWith("@opencode-ai/ai/")) return specifier
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +69,8 @@ export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specif
|
|||||||
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
|
const root = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? specifier)
|
||||||
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
const installed = yield* npm.add(root).pipe(Effect.mapError((cause) => new LoadError({ package: specifier, cause })))
|
||||||
const entrypoint = yield* Effect.try({
|
const entrypoint = yield* Effect.try({
|
||||||
try: () => import.meta.resolve(specifier, pathToFileURL(`${installed.directory}/`).href),
|
try: () =>
|
||||||
|
specifier === root && installed.entrypoint ? installed.entrypoint : resolveModule(specifier, installed.directory),
|
||||||
catch: (cause) => new LoadError({ package: specifier, cause }),
|
catch: (cause) => new LoadError({ package: specifier, cause }),
|
||||||
})
|
})
|
||||||
return yield* importPackage(specifier, entrypoint)
|
return yield* importPackage(specifier, entrypoint)
|
||||||
@@ -116,12 +133,16 @@ export type Info = Provider.Info
|
|||||||
|
|
||||||
export type MutableInfo = DeepMutable<Info>
|
export type MutableInfo = DeepMutable<Info>
|
||||||
|
|
||||||
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (specifier: string, entrypoint: string) {
|
const importPackage = Effect.fn("ProviderV2.importPackage")(function* (
|
||||||
|
specifier: string,
|
||||||
|
entrypoint: string,
|
||||||
|
load = () => importModule(entrypoint),
|
||||||
|
) {
|
||||||
const module = yield* Effect.tryPromise({
|
const module = yield* Effect.tryPromise({
|
||||||
try: () => {
|
try: () => {
|
||||||
const existing = packages.get(entrypoint)
|
const existing = packages.get(entrypoint)
|
||||||
if (existing) return existing
|
if (existing) return existing
|
||||||
const loaded = import(entrypoint)
|
const loaded = load()
|
||||||
packages.set(entrypoint, loaded)
|
packages.set(entrypoint, loaded)
|
||||||
return loaded
|
return loaded
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
// ast-grep-ignore: no-star-import
|
import { createRequire } from "node:module"
|
||||||
import * as pty from "@lydell/node-pty"
|
import { isSea } from "node:sea"
|
||||||
import type { Opts, Proc } from "./pty"
|
import type { Opts, Proc } from "./pty"
|
||||||
|
|
||||||
export type { Disp, Exit, Opts, Proc } from "./pty"
|
export type { Disp, Exit, Opts, Proc } from "./pty"
|
||||||
|
|
||||||
|
const pty = createRequire(import.meta.url)(
|
||||||
|
process.env.OPENCODE_NODE_PTY_PATH ?? "@lydell/node-pty",
|
||||||
|
) as typeof import("@lydell/node-pty")
|
||||||
|
|
||||||
export function spawn(file: string, args: string[], opts: Opts): Proc {
|
export function spawn(file: string, args: string[], opts: Opts): Proc {
|
||||||
const proc = pty.spawn(file, args, opts)
|
const proc = pty.spawn(file, args, process.platform === "win32" && isSea() ? { ...opts, useConptyDll: true } : opts)
|
||||||
return {
|
return {
|
||||||
pid: proc.pid,
|
pid: proc.pid,
|
||||||
onData(listener) {
|
onData(listener) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function importModule(specifier: string) {
|
||||||
|
return import(specifier) as Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModule(specifier: string, directory: string) {
|
||||||
|
return import.meta.resolve(specifier, directory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Script, constants } from "node:vm"
|
||||||
|
import { createRequire, registerHooks } from "node:module"
|
||||||
|
import path from "node:path"
|
||||||
|
import { pathToFileURL } from "node:url"
|
||||||
|
import { resolve, type Package } from "resolve.exports"
|
||||||
|
|
||||||
|
let conditions: readonly string[] = []
|
||||||
|
const conditionHooks = registerHooks({
|
||||||
|
resolve(specifier, context, nextResolve) {
|
||||||
|
conditions = context.conditions
|
||||||
|
return nextResolve(specifier, context)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await new Script('import("node:module")', {
|
||||||
|
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||||
|
}).runInThisContext()
|
||||||
|
conditionHooks.deregister()
|
||||||
|
|
||||||
|
export async function importModule(specifier: string) {
|
||||||
|
const imported = (await new Script(`import(${JSON.stringify(specifier)})`, {
|
||||||
|
importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER,
|
||||||
|
}).runInThisContext()) as unknown
|
||||||
|
if (typeof imported !== "object" || imported === null) return imported
|
||||||
|
|
||||||
|
const module = imported as Record<string, unknown>
|
||||||
|
const exports = module["module.exports"]
|
||||||
|
if (exports !== module.default || (typeof exports !== "object" && typeof exports !== "function") || exports === null)
|
||||||
|
return imported
|
||||||
|
return Object.assign({}, module, exports)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModule(specifier: string, directory: string) {
|
||||||
|
const pkg = createRequire(import.meta.url)(path.join(directory, "package.json")) as Package
|
||||||
|
const target = resolve(pkg, specifier, { conditions, unsafe: true })?.[0]
|
||||||
|
if (target) return pathToFileURL(path.resolve(directory, target)).href
|
||||||
|
const legacyTarget =
|
||||||
|
specifier === pkg.name ? directory : path.resolve(directory, specifier.slice(pkg.name.length + 1))
|
||||||
|
return pathToFileURL(createRequire(path.join(directory, "package.json")).resolve(legacyTarget)).href
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { Global } from "../global"
|
|||||||
import { makeGlobalNode } from "../effect/app-node"
|
import { makeGlobalNode } from "../effect/app-node"
|
||||||
import { httpClient } from "../effect/app-node-platform"
|
import { httpClient } from "../effect/app-node-platform"
|
||||||
import { AbsolutePath } from "../schema"
|
import { AbsolutePath } from "../schema"
|
||||||
|
import { Hash } from "../util/hash"
|
||||||
|
|
||||||
const skillConcurrency = 4
|
const skillConcurrency = 4
|
||||||
const fileConcurrency = 8
|
const fileConcurrency = 8
|
||||||
@@ -110,7 +111,7 @@ const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
if (!data) return []
|
if (!data) return []
|
||||||
|
|
||||||
const sourceRoot = path.resolve(global.cache, "skills", Bun.hash(base).toString(16))
|
const sourceRoot = path.resolve(global.cache, "skills", Hash.fast(base))
|
||||||
return yield* Effect.forEach(
|
return yield* Effect.forEach(
|
||||||
data.skills.flatMap((skill) => {
|
data.skills.flatMap((skill) => {
|
||||||
if (!isSafeSegment(skill.name)) {
|
if (!isSafeSegment(skill.name)) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { dlopen, read, type Pointer } from "bun:ffi"
|
||||||
|
import { existsSync } from "node:fs"
|
||||||
|
|
||||||
|
export type LockResult =
|
||||||
|
| { readonly acquired: true }
|
||||||
|
| { readonly acquired: false; readonly held: true }
|
||||||
|
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||||
|
|
||||||
|
const LOCK_EX = 2
|
||||||
|
const LOCK_NB = 4
|
||||||
|
const DARWIN_EWOULDBLOCK = 35
|
||||||
|
const LINUX_EWOULDBLOCK = 11
|
||||||
|
|
||||||
|
export function lockDarwin(fd: number): LockResult {
|
||||||
|
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
||||||
|
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||||
|
__error: { args: [], returns: "ptr" },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||||
|
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
|
||||||
|
if (result === 0) return { acquired: true }
|
||||||
|
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
||||||
|
return { acquired: false, held: false, code }
|
||||||
|
} finally {
|
||||||
|
library.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lockLinux(fd: number): LockResult {
|
||||||
|
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
|
||||||
|
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
|
||||||
|
flock: { args: ["i32", "i32"], returns: "i32" },
|
||||||
|
__errno_location: { args: [], returns: "ptr" },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
||||||
|
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
|
||||||
|
if (result === 0) return { acquired: true }
|
||||||
|
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
||||||
|
return { acquired: false, held: false, code }
|
||||||
|
} finally {
|
||||||
|
library.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorCode(pointer: Pointer | null) {
|
||||||
|
if (pointer === null) throw new Error("Failed to read process lock error code")
|
||||||
|
return read.i32(pointer, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { dlopen, getInt32 } from "node:ffi"
|
||||||
|
|
||||||
|
export type LockResult =
|
||||||
|
| { readonly acquired: true }
|
||||||
|
| { readonly acquired: false; readonly held: true }
|
||||||
|
| { readonly acquired: false; readonly held: false; readonly code: number }
|
||||||
|
|
||||||
|
const LOCK_EX = 2
|
||||||
|
const LOCK_NB = 4
|
||||||
|
const DARWIN_EWOULDBLOCK = 35
|
||||||
|
const LINUX_EWOULDBLOCK = 11
|
||||||
|
|
||||||
|
export function lockDarwin(fd: number): LockResult {
|
||||||
|
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
||||||
|
flock: { arguments: ["int32", "int32"], return: "int32" },
|
||||||
|
__error: { arguments: [], return: "pointer" },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
|
||||||
|
const code = result === 0 ? 0 : getInt32(library.functions.__error(), 0)
|
||||||
|
if (result === 0) return { acquired: true }
|
||||||
|
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
||||||
|
return { acquired: false, held: false, code }
|
||||||
|
} finally {
|
||||||
|
library.lib.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lockLinux(fd: number): LockResult {
|
||||||
|
const library = dlopen("libc.so.6", {
|
||||||
|
flock: { arguments: ["int32", "int32"], return: "int32" },
|
||||||
|
__errno_location: { arguments: [], return: "pointer" },
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = library.functions.flock(fd, LOCK_EX | LOCK_NB)
|
||||||
|
const code = result === 0 ? 0 : getInt32(library.functions.__errno_location(), 0)
|
||||||
|
if (result === 0) return { acquired: true }
|
||||||
|
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
||||||
|
return { acquired: false, held: false, code }
|
||||||
|
} finally {
|
||||||
|
library.lib.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { dlopen, read, type Pointer } from "bun:ffi"
|
import { lockDarwin, lockLinux, type LockResult } from "#process-lock-ffi"
|
||||||
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs"
|
import { closeSync, mkdirSync, openSync } from "node:fs"
|
||||||
import { connect, createServer, type Server, type Socket } from "node:net"
|
import { connect, createServer, type Server, type Socket } from "node:net"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
@@ -76,60 +76,12 @@ export namespace ProcessLock {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result =
|
function lock(fd: number): LockResult {
|
||||||
| { readonly acquired: true }
|
|
||||||
| { readonly acquired: false; readonly held: true }
|
|
||||||
| { readonly acquired: false; readonly held: false; readonly code: number }
|
|
||||||
|
|
||||||
const LOCK_EX = 2
|
|
||||||
const LOCK_NB = 4
|
|
||||||
const DARWIN_EWOULDBLOCK = 35
|
|
||||||
const LINUX_EWOULDBLOCK = 11
|
|
||||||
|
|
||||||
function lock(fd: number): Result {
|
|
||||||
if (process.platform === "darwin") return lockDarwin(fd)
|
if (process.platform === "darwin") return lockDarwin(fd)
|
||||||
if (process.platform === "linux") return lockLinux(fd)
|
if (process.platform === "linux") return lockLinux(fd)
|
||||||
throw new Error(`Unsupported process lock platform: ${process.platform}`)
|
throw new Error(`Unsupported process lock platform: ${process.platform}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function lockDarwin(fd: number): Result {
|
|
||||||
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
|
||||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
|
||||||
__error: { args: [], returns: "ptr" },
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
|
||||||
const code = result === 0 ? 0 : errorCode(library.symbols.__error())
|
|
||||||
if (result === 0) return { acquired: true }
|
|
||||||
if (code === DARWIN_EWOULDBLOCK) return { acquired: false, held: true }
|
|
||||||
return { acquired: false, held: false, code }
|
|
||||||
} finally {
|
|
||||||
library.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function lockLinux(fd: number): Result {
|
|
||||||
const musl = `/lib/libc.musl-${process.arch === "arm64" ? "aarch64" : "x86_64"}.so.1`
|
|
||||||
const library = dlopen(existsSync(musl) ? musl : "libc.so.6", {
|
|
||||||
flock: { args: ["i32", "i32"], returns: "i32" },
|
|
||||||
__errno_location: { args: [], returns: "ptr" },
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const result = library.symbols.flock(fd, LOCK_EX | LOCK_NB)
|
|
||||||
const code = result === 0 ? 0 : errorCode(library.symbols.__errno_location())
|
|
||||||
if (result === 0) return { acquired: true }
|
|
||||||
if (code === LINUX_EWOULDBLOCK) return { acquired: false, held: true }
|
|
||||||
return { acquired: false, held: false, code }
|
|
||||||
} finally {
|
|
||||||
library.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorCode(pointer: Pointer | null) {
|
|
||||||
if (pointer === null) throw new Error("Failed to read process lock error code")
|
|
||||||
return read.i32(pointer, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function acquireWindows(file: string) {
|
function acquireWindows(file: string) {
|
||||||
return Effect.callback<Server, ProcessLock.LockError>((resume) => {
|
return Effect.callback<Server, ProcessLock.LockError>((resume) => {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.4.3",
|
"@opentui/core": ">=0.4.5",
|
||||||
"@opentui/keymap": ">=0.4.3",
|
"@opentui/keymap": ">=0.4.5",
|
||||||
"@opentui/solid": ">=0.4.3"
|
"@opentui/solid": ">=0.4.5"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@opentui/core": {
|
"@opentui/core": {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"./util/persistence": "./src/util/persistence.ts",
|
"./util/persistence": "./src/util/persistence.ts",
|
||||||
"./util/record": "./src/util/record.ts",
|
"./util/record": "./src/util/record.ts",
|
||||||
"./util/session": "./src/util/session.ts",
|
"./util/session": "./src/util/session.ts",
|
||||||
|
"./util/string-width": "./src/util/string-width.ts",
|
||||||
"./logo": "./src/logo.ts",
|
"./logo": "./src/logo.ts",
|
||||||
"./ui/dialog": "./src/ui/dialog.tsx",
|
"./ui/dialog": "./src/ui/dialog.tsx",
|
||||||
"./ui/spinner": "./src/ui/spinner.ts",
|
"./ui/spinner": "./src/ui/spinner.ts",
|
||||||
@@ -50,6 +51,28 @@
|
|||||||
"./component/spinner": "./src/component/spinner.tsx",
|
"./component/spinner": "./src/component/spinner.tsx",
|
||||||
"./component/register-spinner": "./src/component/register-spinner.ts"
|
"./component/register-spinner": "./src/component/register-spinner.ts"
|
||||||
},
|
},
|
||||||
|
"imports": {
|
||||||
|
"#attention-sounds": {
|
||||||
|
"bun": "./src/attention-sounds.bun.ts",
|
||||||
|
"node": "./src/attention-sounds.node.ts",
|
||||||
|
"default": "./src/attention-sounds.bun.ts"
|
||||||
|
},
|
||||||
|
"#terminal-win32": {
|
||||||
|
"bun": "./src/terminal-win32.bun.ts",
|
||||||
|
"node": "./src/terminal-win32.node.ts",
|
||||||
|
"default": "./src/terminal-win32.bun.ts"
|
||||||
|
},
|
||||||
|
"#string-width": {
|
||||||
|
"bun": "./src/util/string-width.bun.ts",
|
||||||
|
"node": "./src/util/string-width.node.ts",
|
||||||
|
"default": "./src/util/string-width.bun.ts"
|
||||||
|
},
|
||||||
|
"#zed-sqlite": {
|
||||||
|
"bun": "./src/editor-zed-sqlite.bun.ts",
|
||||||
|
"node": "./src/editor-zed-sqlite.node.ts",
|
||||||
|
"default": "./src/editor-zed-sqlite.bun.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
@@ -64,10 +87,12 @@
|
|||||||
"diff": "catalog:",
|
"diff": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
"get-east-asian-width": "catalog:",
|
||||||
"open": "10.1.2",
|
"open": "10.1.2",
|
||||||
"opentui-spinner": "catalog:",
|
"opentui-spinner": "catalog:",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
|
"string-width": "catalog:",
|
||||||
"strip-ansi": "7.1.2",
|
"strip-ansi": "7.1.2",
|
||||||
"uqr": "0.1.3"
|
"uqr": "0.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/// <reference path="./audio.d.ts" />
|
||||||
|
import defaultSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
||||||
|
import questionSoundPath from "@opencode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
|
||||||
|
import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
|
||||||
|
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
|
||||||
|
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
|
||||||
|
|
||||||
|
export { defaultSoundPath, questionSoundPath, permissionSoundPath, errorSoundPath, subagentDoneSoundPath }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createRequire } from "node:module"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const resolve = (name: string) => {
|
||||||
|
const key = `@opencode-ai/ui/audio/${name}`
|
||||||
|
return process.env.OPENCODE_NODE_ASSETS_DIR
|
||||||
|
? path.join(process.env.OPENCODE_NODE_ASSETS_DIR, key)
|
||||||
|
: require.resolve(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSoundPath = resolve("bip-bop-01.mp3")
|
||||||
|
export const questionSoundPath = resolve("bip-bop-03.mp3")
|
||||||
|
export const permissionSoundPath = resolve("staplebops-06.mp3")
|
||||||
|
export const errorSoundPath = resolve("nope-03.mp3")
|
||||||
|
export const subagentDoneSoundPath = resolve("yup-01.mp3")
|
||||||
@@ -14,12 +14,13 @@ import { AttentionSoundName, type Config } from "./config"
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import stripAnsi from "strip-ansi"
|
import stripAnsi from "strip-ansi"
|
||||||
import * as TuiAudio from "./audio"
|
import * as TuiAudio from "./audio"
|
||||||
import defaultSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
import {
|
||||||
import questionSoundPath from "@opencode-ai/ui/audio/bip-bop-03.mp3" with { type: "file" }
|
defaultSoundPath,
|
||||||
import permissionSoundPath from "@opencode-ai/ui/audio/staplebops-06.mp3" with { type: "file" }
|
questionSoundPath,
|
||||||
import errorSoundPath from "@opencode-ai/ui/audio/nope-03.mp3" with { type: "file" }
|
permissionSoundPath,
|
||||||
import doneSoundPath from "@opencode-ai/ui/audio/bip-bop-01.mp3" with { type: "file" }
|
errorSoundPath,
|
||||||
import subagentDoneSoundPath from "@opencode-ai/ui/audio/yup-01.mp3" with { type: "file" }
|
subagentDoneSoundPath,
|
||||||
|
} from "#attention-sounds"
|
||||||
|
|
||||||
type FocusState = "unknown" | "focused" | "blurred"
|
type FocusState = "unknown" | "focused" | "blurred"
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ const BUILTIN_PACK: RegisteredSoundPack = {
|
|||||||
question: questionSoundPath,
|
question: questionSoundPath,
|
||||||
permission: permissionSoundPath,
|
permission: permissionSoundPath,
|
||||||
error: errorSoundPath,
|
error: errorSoundPath,
|
||||||
done: doneSoundPath,
|
done: defaultSoundPath,
|
||||||
subagent_done: subagentDoneSoundPath,
|
subagent_done: subagentDoneSoundPath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { BoxRenderable, TextareaRenderable, ScrollBoxRenderable } from "@opentui/core"
|
import type { BoxRenderable, TextareaRenderable, ScrollBoxRenderable } from "@opentui/core"
|
||||||
import { pathToFileURL } from "bun"
|
import { pathToFileURL } from "node:url"
|
||||||
import fuzzysort from "fuzzysort"
|
import fuzzysort from "fuzzysort"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { firstBy } from "remeda"
|
import { firstBy } from "remeda"
|
||||||
@@ -21,6 +21,7 @@ import { useFrecency } from "../../prompt/frecency"
|
|||||||
import { Keymap } from "../../context/keymap"
|
import { Keymap } from "../../context/keymap"
|
||||||
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
||||||
import type { FileSystemEntry } from "@opencode-ai/client"
|
import type { FileSystemEntry } from "@opencode-ai/client"
|
||||||
|
import { stringWidth } from "../../util/string-width"
|
||||||
|
|
||||||
function removeLineRange(input: string) {
|
function removeLineRange(input: string) {
|
||||||
const hashIndex = input.lastIndexOf("#")
|
const hashIndex = input.lastIndexOf("#")
|
||||||
@@ -188,7 +189,7 @@ export function Autocomplete(props: {
|
|||||||
|
|
||||||
const virtualText = "@" + text
|
const virtualText = "@" + text
|
||||||
const extmarkStart = store.index
|
const extmarkStart = store.index
|
||||||
const extmarkEnd = extmarkStart + Bun.stringWidth(virtualText)
|
const extmarkEnd = extmarkStart + stringWidth(virtualText)
|
||||||
|
|
||||||
const styleId = part.type === "file" ? props.fileStyleId : props.agentStyleId
|
const styleId = part.type === "file" ? props.fileStyleId : props.agentStyleId
|
||||||
|
|
||||||
@@ -431,7 +432,7 @@ export function Autocomplete(props: {
|
|||||||
const cursor = props.input().logicalCursor
|
const cursor = props.input().logicalCursor
|
||||||
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||||
props.input().insertText(newText)
|
props.input().insertText(newText)
|
||||||
props.input().cursorOffset = Bun.stringWidth(newText)
|
props.input().cursorOffset = stringWidth(newText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = createMemo((): AutocompleteOption[] => {
|
const commands = createMemo((): AutocompleteOption[] => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { editorSelectionKey, useEditorContext, type EditorSelection } from "../.
|
|||||||
import { normalizePromptContent, openEditor } from "../../editor"
|
import { normalizePromptContent, openEditor } from "../../editor"
|
||||||
import { useExit } from "../../context/exit"
|
import { useExit } from "../../context/exit"
|
||||||
import { promptOffsetWidth } from "../../prompt/display"
|
import { promptOffsetWidth } from "../../prompt/display"
|
||||||
|
import { stringWidth } from "../../util/string-width"
|
||||||
import { createStore, produce, unwrap } from "solid-js/store"
|
import { createStore, produce, unwrap } from "solid-js/store"
|
||||||
import { emptyPrompt, usePromptHistory, type PromptInfo, type PromptPartRef } from "../../prompt/history"
|
import { emptyPrompt, usePromptHistory, type PromptInfo, type PromptPartRef } from "../../prompt/history"
|
||||||
import { computePromptTraits } from "../../prompt/traits"
|
import { computePromptTraits } from "../../prompt/traits"
|
||||||
@@ -529,7 +530,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
pasted: [],
|
pasted: [],
|
||||||
})
|
})
|
||||||
restoreExtmarksFromPrompt(store.prompt)
|
restoreExtmarksFromPrompt(store.prompt)
|
||||||
input.cursorOffset = Bun.stringWidth(normalized)
|
input.cursorOffset = stringWidth(normalized)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Database } from "bun:sqlite"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
readonly #database: DatabaseSync
|
||||||
|
|
||||||
|
constructor(file: string, options?: { readonly?: boolean }) {
|
||||||
|
this.#database = new DatabaseSync(file, { readOnly: options?.readonly })
|
||||||
|
}
|
||||||
|
|
||||||
|
query(sql: string) {
|
||||||
|
const statement = this.#database.prepare(sql)
|
||||||
|
return {
|
||||||
|
all: (parameters?: Record<string, SQLInputValue>) => (parameters ? statement.all(parameters) : statement.all()),
|
||||||
|
get: (parameters?: Record<string, SQLInputValue>) => (parameters ? statement.get(parameters) : statement.get()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.#database.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Database } from "bun:sqlite"
|
import { Database } from "#zed-sqlite"
|
||||||
import { statSync } from "node:fs"
|
import { statSync } from "node:fs"
|
||||||
import { readFile as readFileAsync } from "node:fs/promises"
|
import { readFile as readFileAsync } from "node:fs/promises"
|
||||||
import os from "node:os"
|
import os from "node:os"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useTuiPaths } from "../../context/runtime"
|
|||||||
import { useTheme } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
import { abbreviateHome } from "../../runtime"
|
import { abbreviateHome } from "../../runtime"
|
||||||
import { FilePath } from "../../ui/file-path"
|
import { FilePath } from "../../ui/file-path"
|
||||||
|
import { stringWidth } from "../../util/string-width"
|
||||||
|
|
||||||
function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||||
const { themeV2 } = useTheme()
|
const { themeV2 } = useTheme()
|
||||||
@@ -56,7 +57,7 @@ function View(props: { context: Plugin.Context }) {
|
|||||||
const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
|
const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
|
||||||
if (list.length === 0) return 0
|
if (list.length === 0) return 0
|
||||||
const count = list.filter((item) => item.status.status === "connected").length
|
const count = list.filter((item) => item.status.status === "connected").length
|
||||||
return Bun.stringWidth(`⊙ ${count} MCP /status`) + 2
|
return stringWidth(`⊙ ${count} MCP /status`) + 2
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,7 +73,7 @@ function View(props: { context: Plugin.Context }) {
|
|||||||
>
|
>
|
||||||
<Directory
|
<Directory
|
||||||
context={props.context}
|
context={props.context}
|
||||||
maxWidth={Math.max(2, dimensions().width - 8 - Bun.stringWidth(InstallationVersion) - mcpWidth())}
|
maxWidth={Math.max(2, dimensions().width - 8 - stringWidth(InstallationVersion) - mcpWidth())}
|
||||||
/>
|
/>
|
||||||
<Mcp context={props.context} />
|
<Mcp context={props.context} />
|
||||||
<box flexGrow={1} />
|
<box flexGrow={1} />
|
||||||
|
|||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
declare module "node:ffi" {
|
||||||
|
type Signature = {
|
||||||
|
readonly arguments?: readonly string[]
|
||||||
|
readonly return?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForeignFunction = (...args: ReadonlyArray<unknown>) => number | bigint
|
||||||
|
|
||||||
|
export function dlopen(
|
||||||
|
path: string,
|
||||||
|
definitions: Readonly<Record<string, Signature>>,
|
||||||
|
): { readonly functions: Readonly<Record<string, ForeignFunction>> }
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { stringWidth } from "../util/string-width"
|
||||||
|
|
||||||
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||||
|
|
||||||
export function promptOffsetWidth(value: string) {
|
export function promptOffsetWidth(value: string) {
|
||||||
let width = 0
|
let width = 0
|
||||||
for (const part of graphemes.segment(value)) {
|
for (const part of graphemes.segment(value)) {
|
||||||
// Textarea offsets count newlines as one position; Bun.stringWidth counts them as zero.
|
// Textarea offsets count newlines as one position; terminal width counts them as zero.
|
||||||
width += part.segment === "\n" ? 1 : Bun.stringWidth(part.segment)
|
width += part.segment === "\n" ? 1 : stringWidth(part.segment)
|
||||||
}
|
}
|
||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type
|
|||||||
import { switchLabel } from "../../util/model"
|
import { switchLabel } from "../../util/model"
|
||||||
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
|
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
|
||||||
import type { ComponentTheme } from "../../theme/v2/component"
|
import type { ComponentTheme } from "../../theme/v2/component"
|
||||||
|
import { stringWidth } from "../../util/string-width"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@@ -1404,8 +1405,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
|||||||
return state() ?? "finished"
|
return state() ?? "finished"
|
||||||
}
|
}
|
||||||
const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
|
const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
|
||||||
const suffix = () =>
|
const suffix = () => Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())))
|
||||||
Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - Bun.stringWidth(heading())))
|
|
||||||
const color = () => {
|
const color = () => {
|
||||||
if (state() === "error") return themeV2.text.feedback.error()
|
if (state() === "error") return themeV2.text.feedback.error()
|
||||||
if (state() === "cancelled") return themeV2.text.feedback.warning()
|
if (state() === "cancelled") return themeV2.text.feedback.warning()
|
||||||
@@ -1561,8 +1561,8 @@ function RevertMessage(props: {
|
|||||||
2,
|
2,
|
||||||
ctx.width -
|
ctx.width -
|
||||||
5 -
|
5 -
|
||||||
(file.additions > 0 ? Bun.stringWidth(`+${file.additions}`) + 1 : 0) -
|
(file.additions > 0 ? stringWidth(`+${file.additions}`) + 1 : 0) -
|
||||||
(file.deletions > 0 ? Bun.stringWidth(`-${file.deletions}`) + 1 : 0),
|
(file.deletions > 0 ? stringWidth(`-${file.deletions}`) + 1 : 0),
|
||||||
)}
|
)}
|
||||||
fg={themeV2.text()}
|
fg={themeV2.text()}
|
||||||
/>
|
/>
|
||||||
@@ -2383,7 +2383,7 @@ function BlockTool(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
<FilePath
|
<FilePath
|
||||||
value={path().value}
|
value={path().value}
|
||||||
maxWidth={Math.max(2, ctx.width - 4 - Bun.stringWidth(path().label) - (props.spinner ? 2 : 0))}
|
maxWidth={Math.max(2, ctx.width - 4 - stringWidth(path().label) - (props.spinner ? 2 : 0))}
|
||||||
fg={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}
|
fg={permission() ? themeV2.text.feedback.warning() : themeV2.text.subdued()}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { dlopen, ptr } from "bun:ffi"
|
||||||
|
import type { ReadStream } from "node:tty"
|
||||||
|
|
||||||
|
const STD_INPUT_HANDLE = -10
|
||||||
|
const ENABLE_PROCESSED_INPUT = 0x0001
|
||||||
|
|
||||||
|
const kernel = () =>
|
||||||
|
dlopen("kernel32.dll", {
|
||||||
|
GetStdHandle: { args: ["i32"], returns: "ptr" },
|
||||||
|
GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
|
||||||
|
SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
|
||||||
|
FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" },
|
||||||
|
})
|
||||||
|
|
||||||
|
let k32: ReturnType<typeof kernel> | undefined
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (process.platform !== "win32") return false
|
||||||
|
try {
|
||||||
|
k32 ??= kernel()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
|
||||||
|
*/
|
||||||
|
export function win32DisableProcessedInput() {
|
||||||
|
if (process.platform !== "win32") return
|
||||||
|
if (!process.stdin.isTTY) return
|
||||||
|
if (!load()) return
|
||||||
|
|
||||||
|
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
||||||
|
const buf = new Uint32Array(1)
|
||||||
|
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
||||||
|
|
||||||
|
const mode = buf[0]!
|
||||||
|
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
||||||
|
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard any queued console input (mouse events, key presses, etc.).
|
||||||
|
*/
|
||||||
|
export function win32FlushInputBuffer() {
|
||||||
|
if (process.platform !== "win32") return
|
||||||
|
if (!process.stdin.isTTY) return
|
||||||
|
if (!load()) return
|
||||||
|
|
||||||
|
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
||||||
|
k32!.symbols.FlushConsoleInputBuffer(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
let unhook: (() => void) | undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep ENABLE_PROCESSED_INPUT disabled.
|
||||||
|
*
|
||||||
|
* On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when
|
||||||
|
* ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes
|
||||||
|
* (sometimes on a later tick), and the flag is console-global, not per-process.
|
||||||
|
*
|
||||||
|
* We combine:
|
||||||
|
* - A `setRawMode(...)` hook to re-clear after known raw-mode toggles.
|
||||||
|
* - A low-frequency poll as a backstop for native/external mode changes.
|
||||||
|
*/
|
||||||
|
export function win32InstallCtrlCGuard() {
|
||||||
|
if (process.platform !== "win32") return
|
||||||
|
if (!process.stdin.isTTY) return
|
||||||
|
if (!load()) return
|
||||||
|
if (unhook) return unhook
|
||||||
|
|
||||||
|
const stdin = process.stdin as ReadStream
|
||||||
|
const original = stdin.setRawMode
|
||||||
|
|
||||||
|
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
||||||
|
const buf = new Uint32Array(1)
|
||||||
|
|
||||||
|
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
||||||
|
const initial = buf[0]!
|
||||||
|
|
||||||
|
const enforce = () => {
|
||||||
|
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
||||||
|
const mode = buf[0]!
|
||||||
|
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
||||||
|
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some runtimes can re-apply console modes on the next tick; enforce twice.
|
||||||
|
const later = () => {
|
||||||
|
enforce()
|
||||||
|
setImmediate(enforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrapped: ReadStream["setRawMode"] | undefined
|
||||||
|
|
||||||
|
if (typeof original === "function") {
|
||||||
|
wrapped = (mode: boolean) => {
|
||||||
|
const result = original.call(stdin, mode)
|
||||||
|
later()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin.setRawMode = wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's cleared immediately too (covers any earlier mode changes).
|
||||||
|
later()
|
||||||
|
|
||||||
|
const interval = setInterval(enforce, 100)
|
||||||
|
interval.unref()
|
||||||
|
|
||||||
|
let done = false
|
||||||
|
unhook = () => {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
|
||||||
|
clearInterval(interval)
|
||||||
|
if (wrapped && stdin.setRawMode === wrapped) {
|
||||||
|
stdin.setRawMode = original
|
||||||
|
}
|
||||||
|
|
||||||
|
k32!.symbols.SetConsoleMode(handle, initial)
|
||||||
|
unhook = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return unhook
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { dlopen } from "node:ffi"
|
||||||
|
import type { ReadStream } from "node:tty"
|
||||||
|
|
||||||
|
const STD_INPUT_HANDLE = -10
|
||||||
|
const ENABLE_PROCESSED_INPUT = 0x0001
|
||||||
|
|
||||||
|
const kernel = () =>
|
||||||
|
dlopen("kernel32.dll", {
|
||||||
|
GetStdHandle: { arguments: ["i32"], return: "pointer" },
|
||||||
|
GetConsoleMode: { arguments: ["pointer", "pointer"], return: "i32" },
|
||||||
|
SetConsoleMode: { arguments: ["pointer", "u32"], return: "i32" },
|
||||||
|
FlushConsoleInputBuffer: { arguments: ["pointer"], return: "i32" },
|
||||||
|
}).functions
|
||||||
|
|
||||||
|
let k32: ReturnType<typeof kernel> | undefined
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (process.platform !== "win32") return false
|
||||||
|
try {
|
||||||
|
k32 ??= kernel()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function win32DisableProcessedInput() {
|
||||||
|
if (process.platform !== "win32" || !process.stdin.isTTY || !load()) return
|
||||||
|
const handle = k32!.GetStdHandle(STD_INPUT_HANDLE)
|
||||||
|
const buffer = new Uint32Array(1)
|
||||||
|
if (k32!.GetConsoleMode(handle, buffer) === 0) return
|
||||||
|
const mode = buffer[0]!
|
||||||
|
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
||||||
|
k32!.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function win32FlushInputBuffer() {
|
||||||
|
if (process.platform !== "win32" || !process.stdin.isTTY || !load()) return
|
||||||
|
k32!.FlushConsoleInputBuffer(k32!.GetStdHandle(STD_INPUT_HANDLE))
|
||||||
|
}
|
||||||
|
|
||||||
|
let unhook: (() => void) | undefined
|
||||||
|
|
||||||
|
export function win32InstallCtrlCGuard() {
|
||||||
|
if (process.platform !== "win32" || !process.stdin.isTTY || !load() || unhook) return unhook
|
||||||
|
const stdin = process.stdin as ReadStream
|
||||||
|
const original = stdin.setRawMode
|
||||||
|
const handle = k32!.GetStdHandle(STD_INPUT_HANDLE)
|
||||||
|
const buffer = new Uint32Array(1)
|
||||||
|
if (k32!.GetConsoleMode(handle, buffer) === 0) return
|
||||||
|
const initial = buffer[0]!
|
||||||
|
const enforce = () => {
|
||||||
|
if (k32!.GetConsoleMode(handle, buffer) === 0) return
|
||||||
|
const mode = buffer[0]!
|
||||||
|
if ((mode & ENABLE_PROCESSED_INPUT) !== 0) k32!.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
||||||
|
}
|
||||||
|
const later = () => {
|
||||||
|
enforce()
|
||||||
|
setImmediate(enforce)
|
||||||
|
}
|
||||||
|
const wrapped: ReadStream["setRawMode"] = (mode) => {
|
||||||
|
const result = original.call(stdin, mode)
|
||||||
|
later()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
stdin.setRawMode = wrapped
|
||||||
|
later()
|
||||||
|
const interval = setInterval(enforce, 100)
|
||||||
|
interval.unref()
|
||||||
|
unhook = () => {
|
||||||
|
clearInterval(interval)
|
||||||
|
if (stdin.setRawMode === wrapped) stdin.setRawMode = original
|
||||||
|
k32!.SetConsoleMode(handle, initial)
|
||||||
|
unhook = undefined
|
||||||
|
}
|
||||||
|
return unhook
|
||||||
|
}
|
||||||
@@ -1,130 +1 @@
|
|||||||
import { dlopen, ptr } from "bun:ffi"
|
export { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "#terminal-win32"
|
||||||
import type { ReadStream } from "node:tty"
|
|
||||||
|
|
||||||
const STD_INPUT_HANDLE = -10
|
|
||||||
const ENABLE_PROCESSED_INPUT = 0x0001
|
|
||||||
|
|
||||||
const kernel = () =>
|
|
||||||
dlopen("kernel32.dll", {
|
|
||||||
GetStdHandle: { args: ["i32"], returns: "ptr" },
|
|
||||||
GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
|
|
||||||
SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
|
|
||||||
FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" },
|
|
||||||
})
|
|
||||||
|
|
||||||
let k32: ReturnType<typeof kernel> | undefined
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
if (process.platform !== "win32") return false
|
|
||||||
try {
|
|
||||||
k32 ??= kernel()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
|
|
||||||
*/
|
|
||||||
export function win32DisableProcessedInput() {
|
|
||||||
if (process.platform !== "win32") return
|
|
||||||
if (!process.stdin.isTTY) return
|
|
||||||
if (!load()) return
|
|
||||||
|
|
||||||
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
|
||||||
const buf = new Uint32Array(1)
|
|
||||||
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
|
||||||
|
|
||||||
const mode = buf[0]!
|
|
||||||
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
|
||||||
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discard any queued console input (mouse events, key presses, etc.).
|
|
||||||
*/
|
|
||||||
export function win32FlushInputBuffer() {
|
|
||||||
if (process.platform !== "win32") return
|
|
||||||
if (!process.stdin.isTTY) return
|
|
||||||
if (!load()) return
|
|
||||||
|
|
||||||
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
|
||||||
k32!.symbols.FlushConsoleInputBuffer(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
let unhook: (() => void) | undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep ENABLE_PROCESSED_INPUT disabled.
|
|
||||||
*
|
|
||||||
* On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when
|
|
||||||
* ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes
|
|
||||||
* (sometimes on a later tick), and the flag is console-global, not per-process.
|
|
||||||
*
|
|
||||||
* We combine:
|
|
||||||
* - A `setRawMode(...)` hook to re-clear after known raw-mode toggles.
|
|
||||||
* - A low-frequency poll as a backstop for native/external mode changes.
|
|
||||||
*/
|
|
||||||
export function win32InstallCtrlCGuard() {
|
|
||||||
if (process.platform !== "win32") return
|
|
||||||
if (!process.stdin.isTTY) return
|
|
||||||
if (!load()) return
|
|
||||||
if (unhook) return unhook
|
|
||||||
|
|
||||||
const stdin = process.stdin as ReadStream
|
|
||||||
const original = stdin.setRawMode
|
|
||||||
|
|
||||||
const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
|
|
||||||
const buf = new Uint32Array(1)
|
|
||||||
|
|
||||||
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
|
||||||
const initial = buf[0]!
|
|
||||||
|
|
||||||
const enforce = () => {
|
|
||||||
if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
|
|
||||||
const mode = buf[0]!
|
|
||||||
if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
|
|
||||||
k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some runtimes can re-apply console modes on the next tick; enforce twice.
|
|
||||||
const later = () => {
|
|
||||||
enforce()
|
|
||||||
setImmediate(enforce)
|
|
||||||
}
|
|
||||||
|
|
||||||
let wrapped: ReadStream["setRawMode"] | undefined
|
|
||||||
|
|
||||||
if (typeof original === "function") {
|
|
||||||
wrapped = (mode: boolean) => {
|
|
||||||
const result = original.call(stdin, mode)
|
|
||||||
later()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
stdin.setRawMode = wrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure it's cleared immediately too (covers any earlier mode changes).
|
|
||||||
later()
|
|
||||||
|
|
||||||
const interval = setInterval(enforce, 100)
|
|
||||||
interval.unref()
|
|
||||||
|
|
||||||
let done = false
|
|
||||||
unhook = () => {
|
|
||||||
if (done) return
|
|
||||||
done = true
|
|
||||||
|
|
||||||
clearInterval(interval)
|
|
||||||
if (wrapped && stdin.setRawMode === wrapped) {
|
|
||||||
stdin.setRawMode = original
|
|
||||||
}
|
|
||||||
|
|
||||||
k32!.symbols.SetConsoleMode(handle, initial)
|
|
||||||
unhook = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return unhook
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RGBA } from "@opentui/core"
|
import type { RGBA } from "@opentui/core"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
|
import { stringWidth } from "../util/string-width"
|
||||||
|
|
||||||
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ export function FilePath(props: FilePathProps) {
|
|||||||
|
|
||||||
export function truncateFilePath(value: string, maxWidth: number) {
|
export function truncateFilePath(value: string, maxWidth: number) {
|
||||||
if (maxWidth <= 0) return ""
|
if (maxWidth <= 0) return ""
|
||||||
if (Bun.stringWidth(value) <= maxWidth) return value
|
if (stringWidth(value) <= maxWidth) return value
|
||||||
|
|
||||||
const drive = value.match(/^([A-Za-z]:)([\\/])/)
|
const drive = value.match(/^([A-Za-z]:)([\\/])/)
|
||||||
const unc = value.match(/^(\\\\|\/\/)([^\\/]+)[\\/]([^\\/]+)(?:[\\/]|$)/)
|
const unc = value.match(/^(\\\\|\/\/)([^\\/]+)[\\/]([^\\/]+)(?:[\\/]|$)/)
|
||||||
@@ -46,22 +47,22 @@ export function truncateFilePath(value: string, maxWidth: number) {
|
|||||||
const segments = source.split(windows ? /[\\/]/ : separator).filter(Boolean)
|
const segments = source.split(windows ? /[\\/]/ : separator).filter(Boolean)
|
||||||
const basename = segments.at(-1) ?? value
|
const basename = segments.at(-1) ?? value
|
||||||
if (segments.length < 2) {
|
if (segments.length < 2) {
|
||||||
const rootWidth = Bun.stringWidth(root)
|
const rootWidth = stringWidth(root)
|
||||||
if (rootWidth >= maxWidth) return takeStart(root, maxWidth)
|
if (rootWidth >= maxWidth) return takeStart(root, maxWidth)
|
||||||
return root + truncateBasename(basename, maxWidth - rootWidth)
|
return root + truncateBasename(basename, maxWidth - rootWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = `${root}…${separator}`
|
const prefix = `${root}…${separator}`
|
||||||
const basenameWidth = maxWidth - Bun.stringWidth(prefix)
|
const basenameWidth = maxWidth - stringWidth(prefix)
|
||||||
if (basenameWidth <= 0) return takeStart(prefix, maxWidth)
|
if (basenameWidth <= 0) return takeStart(prefix, maxWidth)
|
||||||
const compact = truncateBasename(basename, basenameWidth)
|
const compact = truncateBasename(basename, basenameWidth)
|
||||||
if (compact !== basename) return prefix + compact
|
if (compact !== basename) return prefix + compact
|
||||||
|
|
||||||
const selected = [basename]
|
const selected = [basename]
|
||||||
const separatorWidth = Bun.stringWidth(separator)
|
const separatorWidth = stringWidth(separator)
|
||||||
let width = Bun.stringWidth(prefix + basename)
|
let width = stringWidth(prefix + basename)
|
||||||
for (let index = segments.length - 2; index >= 0; index--) {
|
for (let index = segments.length - 2; index >= 0; index--) {
|
||||||
const next = Bun.stringWidth(segments[index]!) + separatorWidth
|
const next = stringWidth(segments[index]!) + separatorWidth
|
||||||
if (width + next > maxWidth) break
|
if (width + next > maxWidth) break
|
||||||
selected.unshift(segments[index]!)
|
selected.unshift(segments[index]!)
|
||||||
width += next
|
width += next
|
||||||
@@ -70,12 +71,12 @@ export function truncateFilePath(value: string, maxWidth: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function truncateBasename(value: string, maxWidth: number) {
|
function truncateBasename(value: string, maxWidth: number) {
|
||||||
if (Bun.stringWidth(value) <= maxWidth) return value
|
if (stringWidth(value) <= maxWidth) return value
|
||||||
if (maxWidth <= 1) return takeStart("…", maxWidth)
|
if (maxWidth <= 1) return takeStart("…", maxWidth)
|
||||||
|
|
||||||
const dot = value.lastIndexOf(".")
|
const dot = value.lastIndexOf(".")
|
||||||
const extension = dot > 0 ? value.slice(dot) : ""
|
const extension = dot > 0 ? value.slice(dot) : ""
|
||||||
const extensionWidth = Bun.stringWidth(extension)
|
const extensionWidth = stringWidth(extension)
|
||||||
if (extensionWidth >= maxWidth) return "…" + takeEnd(extension, maxWidth - 1)
|
if (extensionWidth >= maxWidth) return "…" + takeEnd(extension, maxWidth - 1)
|
||||||
|
|
||||||
const stem = extension ? value.slice(0, dot) : value
|
const stem = extension ? value.slice(0, dot) : value
|
||||||
@@ -96,7 +97,7 @@ function take(value: string, maxWidth: number, reverse: boolean) {
|
|||||||
const selected: string[] = []
|
const selected: string[] = []
|
||||||
let width = 0
|
let width = 0
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
const next = Bun.stringWidth(segment)
|
const next = stringWidth(segment)
|
||||||
if (width + next > maxWidth) break
|
if (width + next > maxWidth) break
|
||||||
selected.push(segment)
|
selected.push(segment)
|
||||||
width += next
|
width += next
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { stringWidth } from "./string-width"
|
||||||
|
|
||||||
export function titlecase(str: string) {
|
export function titlecase(str: string) {
|
||||||
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
||||||
}
|
}
|
||||||
@@ -67,13 +69,13 @@ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme
|
|||||||
|
|
||||||
export function truncateWidth(str: string, width: number): string {
|
export function truncateWidth(str: string, width: number): string {
|
||||||
if (width <= 0) return ""
|
if (width <= 0) return ""
|
||||||
if (Bun.stringWidth(str) <= width) return str
|
if (stringWidth(str) <= width) return str
|
||||||
if (width === 1) return "…"
|
if (width === 1) return "…"
|
||||||
|
|
||||||
const result: string[] = []
|
const result: string[] = []
|
||||||
let used = 0
|
let used = 0
|
||||||
for (const item of graphemeSegmenter.segment(str)) {
|
for (const item of graphemeSegmenter.segment(str)) {
|
||||||
const next = Bun.stringWidth(item.segment)
|
const next = stringWidth(item.segment)
|
||||||
if (used + next > width - 1) break
|
if (used + next > width - 1) break
|
||||||
result.push(item.segment)
|
result.push(item.segment)
|
||||||
used += next
|
used += next
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { appendFile, mkdir, rename, rm } from "fs/promises"
|
import { appendFile, mkdir, readFile, rename, rm, writeFile } from "fs/promises"
|
||||||
|
|
||||||
export function readText(filePath: string) {
|
export function readText(filePath: string) {
|
||||||
return Bun.file(filePath).text()
|
return readFile(filePath, "utf8")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readJson<T>(filePath: string) {
|
export async function readJson<T>(filePath: string) {
|
||||||
return Bun.file(filePath).json() as Promise<T>
|
return JSON.parse(await readFile(filePath, "utf8")) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writeText(filePath: string, content: string) {
|
export async function writeText(filePath: string, content: string) {
|
||||||
await mkdir(path.dirname(filePath), { recursive: true })
|
await mkdir(path.dirname(filePath), { recursive: true })
|
||||||
await Bun.write(filePath, content)
|
await writeFile(filePath, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function appendText(filePath: string, content: string) {
|
export async function appendText(filePath: string, content: string) {
|
||||||
@@ -22,7 +22,7 @@ export async function appendText(filePath: string, content: string) {
|
|||||||
export async function writeJsonAtomic(filePath: string, value: unknown) {
|
export async function writeJsonAtomic(filePath: string, value: unknown) {
|
||||||
await mkdir(path.dirname(filePath), { recursive: true })
|
await mkdir(path.dirname(filePath), { recursive: true })
|
||||||
const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`
|
const temporary = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`
|
||||||
await Bun.write(temporary, JSON.stringify(value)).catch(async (error) => {
|
await writeFile(temporary, JSON.stringify(value)).catch(async (error) => {
|
||||||
await rm(temporary, { force: true }).catch(() => undefined)
|
await rm(temporary, { force: true }).catch(() => undefined)
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const stringWidth = Bun.stringWidth
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import measure from "string-width"
|
||||||
|
import stripAnsi from "strip-ansi"
|
||||||
|
import { eastAsianWidth } from "get-east-asian-width"
|
||||||
|
|
||||||
|
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
||||||
|
const textEmoji = /^\p{Emoji}\p{Mark}*$/u
|
||||||
|
const emojiPresentation = /^\p{Emoji_Presentation}/u
|
||||||
|
|
||||||
|
export function stringWidth(value: string) {
|
||||||
|
return Array.from(graphemes.segment(stripAnsi(value))).reduce((total, part) => {
|
||||||
|
const width = measure(part.segment)
|
||||||
|
const codePoint = part.segment.codePointAt(0)
|
||||||
|
if (
|
||||||
|
width !== 2 ||
|
||||||
|
codePoint === undefined ||
|
||||||
|
eastAsianWidth(codePoint) === 2 ||
|
||||||
|
!textEmoji.test(part.segment) ||
|
||||||
|
emojiPresentation.test(part.segment) ||
|
||||||
|
part.segment.includes("\uFE0F") ||
|
||||||
|
part.segment.includes("\u20E3") ||
|
||||||
|
part.segment.includes("\u200D")
|
||||||
|
)
|
||||||
|
return total + width
|
||||||
|
return total + 1
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { stringWidth } from "#string-width"
|
||||||
@@ -170,7 +170,7 @@ async function renderOnceSettled(app: Awaited<ReturnType<typeof testRender>>) {
|
|||||||
async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>) {
|
async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>) {
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
if (frame.trim().length > 0) return frame
|
if (visibleLines(frame).length > 0) return frame
|
||||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user