Compare commits

..
7 Commits
Author SHA1 Message Date
EFLKumo 241a5ba417 bump version to 0.1.2 2026-09-19 18:49:48 +08:00
EFLKumo f91996ddf6 make it self-contained 2026-09-19 18:49:48 +08:00
EFLKumo 6a9318568c setup tsdown 2026-09-19 18:49:48 +08:00
EFLKumo 12b2f73596 Add project README 2026-09-19 18:49:48 +08:00
EFLKumo ad5c0fa91c EFFECT.md: Effect 4 API shapes verified against repos/effect
Agents kept re-checking the same v4 signatures (renames from Effect 3,
Schedule, CLI flags, MCP, vitest, vscode-jsonrpc failure modes). Record
them once and point to the file from CLAUDE.md.
2026-09-19 18:49:48 +08:00
EFLKumo b028ba8e03 Restart a crashed language server per --restart never|always|N
The process is supervised: on exit after a successful initialize it is
spawned again with exponential backoff (500ms to 30s), open documents are
re-sent as didOpen before the new session is announced, and requests caught
by the crash wait for it and retry. A crash more than a minute after the
previous one starts a fresh streak; reaching the streak limit (default 3)
ends restarting for good, after which every request fails with an LspError
saying so. Restarts and give-ups are logged and forwarded to the MCP client.

Writes to the child's stdin go through a PassThrough so a broken pipe ends
the session instead of surfacing as an unhandled rejection.
2026-09-19 18:49:48 +08:00
EFLKumo 942074cf85 small fixes to remove as never 2026-09-19 18:49:48 +08:00
15 changed files with 1182 additions and 172 deletions

No files matched your search

+2 -1
View File
@@ -1,2 +1,3 @@
node_modules
node_modules/
dist/
repos/
+17 -6
View File
@@ -2,15 +2,17 @@
An MCP server that exposes one Language Server to agents as a headless editor.
Effect 4 (`effect/unstable/ai` for MCP, `effect/unstable/cli` for the CLI) plus
`vscode-languageserver-protocol` for LSP. TypeScript runs directly on Node 26
(type stripping); there is no build step.
`vscode-languageserver-protocol` for LSP. TypeScript is bundled for Node 26 with
tsdown; source files can still run directly through Node's type stripping.
## Commands
- `pnpm tsc` — type-check (tsgo with the Effect language-service plugin)
- `pnpm build` — bundle the CLI to `dist/main.mjs` with tsdown
- `pnpm dev` — run the TypeScript source directly
- `pnpm test` — vitest; integration tests spawn `test/fixtures/server.ts`, a real LSP server
- `pnpm biome check --write --unsafe` — lint/format (run before committing; its rewrites are safe)
- `node src/main.ts --help` — the CLI; smoke-test with `node src/main.ts --root <dir> -- <server> [args]`
- `node dist/main.mjs --help` — the built CLI; smoke-test with `node dist/main.mjs --root <dir> -- <server> [args]`
- Commit with `jj commit -m "..."`, never `git`
The shell may have an HTTP proxy configured; when curling the `--http` transport locally, pass `--noproxy '*'`.
@@ -24,7 +26,8 @@ src/Resources.ts MCP resources (lsp://server, lsp://diagnostics[/{file}]) a
src/Server.ts layer composition: LanguageServer → Documents+Diagnostics → transport → tools/resources
src/main.ts CLI entry (Command/Flag/Argument), logs to stderr, NodeRuntime.runMain
src/lsp/ the ONLY place vscode-* packages are used at runtime
LanguageServer.ts spawn + JSON-RPC + initialize/shutdown; request() waits for $/progress idle and retries ContentModified
LanguageServer.ts spawn + JSON-RPC + initialize/shutdown, supervised: restarts on crash per policy, reopens documents
request() waits for $/progress idle and retries ContentModified / a server that is being restarted
Documents.ts on-demand didOpen, LRU via ScopedCache, disk change detection → didChange+didSave, applyEdit
Diagnostics.ts publishDiagnostics accumulation; pull (textDocument/diagnostic) or settled push per doc version
Editor.ts the operations = MCP tool handlers; LSP results → Model values
@@ -34,6 +37,9 @@ test/fixtures/workspace.ts temp workspace layer running the fixture server
Rule: outside `src/lsp/`, `vscode-*` may only be imported as types.
`EFFECT.md` lists the Effect 4 API shapes already verified against `repos/effect` (renames from Effect 3, Schedule,
CLI, MCP, vitest). Read it before grepping the Effect source, and add to it when you verify something new.
## Design decisions worth knowing
- Positions handed to agents are 1-based; `Where` accepts `column` or a `symbol` name to find on the line.
@@ -48,6 +54,12 @@ Rule: outside `src/lsp/`, `vscode-*` may only be imported as types.
plus a 250ms quiet period, bounded by 5s, memoized per (uri, version).
- Tests that talk to the fixture server use `it.layer(..., { excludeTestServices: true })` so the real
clock is used (Stream.debounce / timeouts would hang under TestClock).
- Crashes: `--restart never|always|N` (default 3). A crash is the process exiting or closing the connection after a
successful initialize; a server that cannot initialize fails lsp-mcp immediately. Restarts back off from 500ms to 30s,
a crash more than a minute after the previous one starts a new streak, and the streak limit `N` ends restarting for good.
Requests in flight during a crash wait for the new process and retry; documents are re-`didOpen`ed with their current
text; the capabilities and tool set stay those of the first process. Each restart and the final give-up are logged and
sent to the MCP client as `notifications/message`. Once given up, every tool call fails with an `LspError` that says so.
## Status
@@ -56,5 +68,4 @@ verified against the fixture server and `typescript-language-server`. `goal.md`
no longer authoritative; the code is.
Ideas not done: inlay hints, type hierarchy, semantic tokens, partial results, workspace/didChangeWatchedFiles
when a server insists on it, restarting a crashed language server (currently requests fail with a clear
`LspError` until the MCP client restarts the process).
when a server insists on it.
+85
View File
@@ -0,0 +1,85 @@
# Effect 4 notes for this repo
Effect `4.0.0-rc.115` (pinned in `package.json`; `repos/effect` is the same version, check
`repos/effect/packages/effect/package.json` if either is bumped). The API differs from Effect 3
in places, and the type stripper gives no hints, so agents kept re-verifying the same shapes.
This file records what has been verified against the source so the next agent does not have to.
When something is not listed here, grep the source, not `node_modules`:
`grep -n "^export const NAME" repos/effect/packages/effect/src/MODULE.ts` shows the signature
and implementation; `repos/effect/packages/effect/test/` has usage examples. Add what you learn.
## Renames from Effect 3 (the things that bite)
- `Effect.async` → `Effect.callback`; `Effect.catchAll` → `Effect.catch`; `Effect.catchAllCause` → `Effect.catchCause`.
- Every `unsafeX` is `xUnsafe`: `Deferred.doneUnsafe`, `Deferred.isDoneUnsafe`, `PubSub.publishUnsafe`,
`SubscriptionRef.getUnsafe`, `Latch.openUnsafe`, `Scope.makeUnsafe`.
- `Context.Tag` classes are `Context.Service<Self, Shape>()("id")`; layers are built with `Layer.effect(Tag, make)`.
- `Schema.Union([A, B])` and `Schema.Literals(["a", "b"])` take an array. Filters are `.check(Schema.isInt(),
Schema.isGreaterThanOrEqualTo(0))`, not `.pipe(Schema.int())`. `.annotate({ description, message })`.
`Schema.TaggedError<Self>()("Tag", fields)`; `Schema.NumberFromString`; `Schema.fromJsonString(Schema.Json)`.
- `Duration.isGreaterThan` / `isLessThan` (not `greaterThan`); `Duration.format(d)` prints `1s 500ms`.
- `Effect.timeout` / `timeoutOption` / `timeoutOrElse`; `Stream.runHead` returns an `Option`.
- `Effect.forkScoped(effect, { startImmediately?, uninterruptible? })`; `Effect.forkIn(scope)`; `Effect.forkDetach`.
- `Effect.tap` accepts an effect or a function; `Effect.when(condition)` returns an `Option`.
- `Effect.fn("name")(function* (...) {})` for traced generator functions (used everywhere in `src/lsp/`).
## Schedule (reshaped in v4)
- Constructors: `exponential(base, factor)`, `spaced`, `fixed`, `recurs(n)`, `forever`, `jittered`, `cron`.
- Limits: `Schedule.upTo({ times, duration })` (replaces `intersect(recurs(n))`); `Schedule.while(meta => boolean | Effect)`
replaces `whileInput` / `whileOutput` / `recurWhile`; `Schedule.modifyDelay(meta => Duration.Input)`; `Schedule.tap(meta => ...)`;
`Schedule.passthrough`. Combinators: `Schedule.max(a, b)` recurs while both do, with the slower delay (Effect 3
`intersect`); `Schedule.min` recurs while either does, with the faster delay (`union`); `Schedule.concat` runs one
then the other (`andThen`). There is no `resetAfter`: write a step with its own state instead (below).
- `Metadata` given to those callbacks: `{ input, output, duration, attempt, start, now, elapsed, elapsedSincePrevious }`.
`elapsedSincePrevious` is the time between consecutive steps (between crashes, for a restart policy).
- Custom schedules: `Schedule.fromStepWithMetadata(Effect.sync(() => (meta: InputMetadata<Input>) => ...))`, where the step
returns `Effect.succeed([output, delay])` to continue or `Cause.done(output)` to stop. `Effect.sync` runs once per `retry`,
so closure state (a crash streak) is per run. See `restarts` in `src/lsp/LanguageServer.ts`.
- `Effect.retry` takes `{ while, until, times, schedule }`, a `Schedule`, or a builder `($) => ...`. `Effect.retryOrElse(schedule,
(lastError, output) => ...)` runs the fallback once the schedule stops. The effect under retry/repeat sees
`Schedule.CurrentMetadata` as a service.
## Concurrency primitives (as expected, with v4 names)
- `Deferred.make<A, E>()`, `await`, `succeed`, `fail`, `done(exit)`, `isDone`; the `*Unsafe` variants are synchronous and return
`boolean` (whether this call completed it). Waiters of `Deferred.await` resume synchronously inside `doneUnsafe`.
- `PubSub.unbounded<A>({ replay? })`, `publish`, `publishUnsafe`, `subscribe`, `Stream.fromPubSub`, `Stream.fromSubscription`.
- `SubscriptionRef.make/get/set/update/changes`; `changes` emits the current value first (replay 1).
- `Latch.make(open?)` with `.open`, `.close`, `.await`, `.release`, `.whenOpen(effect)`.
- `Semaphore.make(n)`, `.withPermits(n)(effect)`; `FiberSet.makeRuntime<R>()` / `makeRuntimePromise` to run effects from callbacks.
- `ScopedCache.makeWith({ capacity, timeToLive: (exit) => Duration, lookup })`, `get`, `has`, `invalidate`, `keys`, `values`, `entries`.
- `Cause.done(value)` is the halt signal used by `Pull`, `Schedule` and `Stream` internals; `Cause.hasInterruptsOnly(cause)`.
## `effect/unstable/cli`
- `Command.make(name, { flag: Flag..., arg: Argument... }, Effect.fn(function* (config) {}))`, `Command.withDescription`,
`Command.withExamples([{ command, description }])`, `Command.run({ version })`.
- `Flag.String/Int/Boolean/Directory(name, { mustExist })/KeyValuePair/Literals(name, [...])`; `Flag.withSchema(schema)`
(decodes from the flag's type, so `Schema.NumberFromString` for a string flag), `withDescription`, `withDefault`, `withAlias`,
`withMetavar("never|always|N")`, `optional` (→ `Option`), `map`, `orElse`. `Argument.String`, `Argument.variadic`.
- Help does not print default values; put them in the description. Schema failures print `Invalid value for flag --x: "v".
Expected: Schema validation failed: <message annotation or issue>`.
## `effect/unstable/ai` MCP (see `src/Server.ts`, `src/Tools.ts`, `src/Resources.ts`)
- `McpServer.layerStdio(info)` / `layerHttp({ ...info, path })` served with `HttpRouter.serve`; `McpServer.registerToolkit(toolkit)`
after `Toolkit.toLayer(handlers)`; `McpServer.registerResource({ uri | template, name, description, mimeType, content, completion })`.
- `Tool.make(name, { description, parameters, success, failure, dependencies }).annotate(Tool.Readonly, ...)`.
- `McpServer.McpServer` service exposes `notifications["notifications/message" | "notifications/resources/updated"]`.
## `@effect/vitest`
- `it.effect` / `it.live` tests run inside a `Scope`; `it.layer(layer, { excludeTestServices: true })("name", (it) => ...)` shares the
layer across the block and, with that option, uses the real clock (`Stream.debounce` and timeouts hang under `TestClock`).
- Real processes and `Effect.sleep` need real time, so every fixture-server test uses `excludeTestServices` or `it.live`.
## `vscode-jsonrpc` (not Effect, but verified the same way)
- Loss of the peer: `connection.onClose` fires on transport close and on `dispose()`; pending requests reject with
`ResponseError` codes `ConnectionInactive` (-32096), `PendingResponseRejected` (-32097) or `MessageWriteError` (-32099).
- Writing to a dead child's stdin rejects a promise nobody handles (EPIPE crashes the process). `LanguageServer.ts` writes through
a `PassThrough` piped into stdin and listens for stdin errors instead.
- `connection.onRequest((method, params) => ...)` registers a catch-all ("star") handler; handlers registered per type are
consulted first.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 EFLKumo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+132
View File
@@ -0,0 +1,132 @@
# lsp-mcp
An [MCP](https://modelcontextprotocol.io/) server that exposes a single [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) server to agents as a headless editor.
`lsp-mcp` starts the language server you provide, keeps its document state in sync with the workspace, and exposes the language server's capabilities through MCP tools and resources. It is written in TypeScript and [Effect](https://effect.website).
## Features
- stdio and Streamable HTTP MCP transports
- Diagnostics exposed both as a tool and subscribable MCP resources
- Preview-by-default edits for rename and formatting, with explicit disk application
- Language-server crash supervision, restart backoff, and document reopening
- Initialization options, workspace settings, language-id mappings, and open-document capacity configuration
Underlying techniques:
- On-demand document opening, disk change detection, and an LRU cache of open documents
- 1-based line and column positions designed for agent-facing use
- Capability-aware tool registration: unsupported LSP features are not advertised
## Requirements
- Node.js 26+
- pnpm 12.4.2+
- An installed language server executable
## Installation
```sh
pnpm install
pnpm build
```
From a checkout, invoke the built server with `node dist/main.mjs`. Use `pnpm dev` to run the TypeScript source directly during development.
## Usage
The general form is:
```sh
node dist/main.mjs [lsp-mcp options] -- <language-server> [language-server args...]
```
The `--` separates `lsp-mcp` options from the executable and arguments passed to the language server.
### stdio
stdio is the default transport and is suitable for MCP clients that launch a local server process:
```sh
node dist/main.mjs --root /path/to/project -- typescript-language-server --stdio
```
For example, an MCP client configuration can point at the checkout like this:
```json
{
"mcpServers": {
"typescript": {
"command": "node",
"args": [
"/path/to/lsp-mcp/dist/main.mjs",
"--root",
"/path/to/project",
"--",
"typescript-language-server",
"--stdio"
]
}
}
}
```
### Streamable HTTP
Run the MCP endpoint on a local port with `--http`:
```sh
node dist/main.mjs --http 9010 --root ./crate -- rust-analyzer
```
The endpoint is available at:
```text
http://localhost:9010/mcp
```
### Configuration examples
```sh
# Pass JSON initialization options to the language server.
node dist/main.mjs \
--init-options '{"typescript":{"useInferredProjectPerProjectRoot":true}}' \
-- typescript-language-server --stdio
# Serve workspace/configuration and didChangeConfiguration with JSON settings.
node dist/main.mjs \
--settings '{"gopls":{"staticcheck":true}}' \
-- gopls
# Add a file-extension to language-id mapping.
node dist/main.mjs --language vue=vue --root ./web -- vue-language-server --stdio
# Keep more documents open, and always restart a crashed language server.
node dist/main.mjs --open-documents 64 --restart always -- clangd
```
## Development
```sh
pnpm test # Run the Vitest suite, including the fixture LSP integration tests
pnpm check # Type-check without emitting JavaScript
pnpm lint # Format and lint with Biome
```
The main source layout is:
```text
src/Model.ts Agent-facing schemas and 1-based positions
src/Tools.ts MCP tool definitions and capability gating
src/Resources.ts MCP resources and notifications
src/Server.ts Effect layer composition and transports
src/main.ts CLI entry point
src/lsp/ LSP process, document, diagnostic, and editor bridge
test/ Unit and integration tests with a fixture language server
```
The `vscode-*` packages are intentionally confined to `src/lsp/`; the rest of the application uses the agent-facing schemas and Effect services.
## License
MIT
+16 -12
View File
@@ -1,14 +1,19 @@
{
"name": "lsp-mcp",
"version": "0.1.0",
"name": "@opticlm/lsp-mcp",
"version": "0.1.2",
"description": "MCP server exposing a Language Server's capabilities to agents as a headless editor",
"type": "module",
"license": "MIT",
"files": [
"dist"
],
"bin": {
"lsp-mcp": "./src/main.ts"
"lsp-mcp": "./dist/main.mjs"
},
"scripts": {
"start": "node src/main.ts",
"build": "tsdown",
"start": "node dist/main.mjs",
"dev": "node src/main.ts",
"check": "tsc",
"test": "vitest run",
"lint": "biome check --write --unsafe",
@@ -22,20 +27,19 @@
}
},
"packageManager": "pnpm@12.4.2",
"dependencies": {
"@effect/platform-node": "4.0.0-rc.115",
"effect": "4.0.0-rc.115",
"vscode-languageserver-protocol": "^3.18.3",
"vscode-languageserver-textdocument": "^1.0.14",
"vscode-uri": "^3.2.0"
},
"devDependencies": {
"@biomejs/biome": "2.5.14",
"@effect/platform-node": "4.0.0-rc.115",
"@effect/tsgo": "0.45.0",
"@effect/vitest": "4.0.0-rc.115",
"@types/node": "^26.6.1",
"effect": "4.0.0-rc.115",
"tsdown": "0.23.0",
"typescript": "^7.0.2",
"vitest": "^5.0.1",
"vscode-languageserver": "^10.1.1"
"vscode-languageserver": "^10.1.1",
"vscode-languageserver-protocol": "^3.18.3",
"vscode-languageserver-textdocument": "^1.0.14",
"vscode-uri": "^3.2.0"
}
}
+466 -16
View File
@@ -165,26 +165,13 @@ settings:
importers:
.:
dependencies:
'@effect/platform-node':
specifier: 4.0.0-rc.115
version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1)
effect:
specifier: 4.0.0-rc.115
version: 4.0.0-rc.115
vscode-languageserver-protocol:
specifier: ^3.18.3
version: 3.18.3
vscode-languageserver-textdocument:
specifier: ^1.0.14
version: 1.0.14
vscode-uri:
specifier: ^3.2.0
version: 3.2.0
devDependencies:
'@biomejs/biome':
specifier: 2.5.14
version: 2.5.14
'@effect/platform-node':
specifier: 4.0.0-rc.115
version: 4.0.0-rc.115(effect@4.0.0-rc.115)(redis@6.2.1)
'@effect/tsgo':
specifier: 0.45.0
version: 0.45.0
@@ -194,6 +181,12 @@ importers:
'@types/node':
specifier: ^26.6.1
version: 26.6.1
effect:
specifier: 4.0.0-rc.115
version: 4.0.0-rc.115
tsdown:
specifier: 0.23.0
version: 0.23.0(typescript@7.0.2)
typescript:
specifier: ^7.0.2
version: 7.0.2
@@ -203,6 +196,15 @@ importers:
vscode-languageserver:
specifier: ^10.1.1
version: 10.1.1
vscode-languageserver-protocol:
specifier: ^3.18.3
version: 3.18.3
vscode-languageserver-textdocument:
specifier: ^1.0.14
version: 1.0.14
vscode-uri:
specifier: ^3.2.0
version: 3.2.0
packages:
@@ -334,6 +336,9 @@ packages:
'@oxc-project/types@0.150.0':
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
'@quansync/fs@1.1.0':
resolution: {integrity: sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg==}
'@redis/bloom@6.2.1':
resolution: {integrity: sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag==}
engines: {node: '>= 20.0.0'}
@@ -618,10 +623,149 @@ packages:
'@vitest/spy@5.0.1':
resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
'@yuku-codegen/binding-android-arm64@0.10.2':
resolution: {integrity: sha512-Oc2KInVkPfUjEB4PeV6X0NvIoyYTzDe/WmqFrTwBqKs6YKnz4A7XwWQvJTb7M0rLz9a6WgpUUiiS5QKaCi4O/g==}
cpu: [arm64]
os: [android]
'@yuku-codegen/binding-darwin-arm64@0.10.2':
resolution: {integrity: sha512-3H7eNPIHJndUJXZ4QUGBPq1zC6RGcUZfm7bymJes+/N7wTVWUn1bxZ9JcozYHpkWQNm9f23G8YWbaHD9aYH72A==}
cpu: [arm64]
os: [darwin]
'@yuku-codegen/binding-darwin-x64@0.10.2':
resolution: {integrity: sha512-AJOUpR2s3LF9AWiiub/Uyc/UoXNTWcq8hK3cVI6fUSXtwh34dG21J6hRuNlO6JVJFTo4o3ScVvZOrh/YFfUEAA==}
cpu: [x64]
os: [darwin]
'@yuku-codegen/binding-freebsd-x64@0.10.2':
resolution: {integrity: sha512-ms7DcZu87u5CiK/wMwvpKAvAmtaqKjCQIXNweMLypG6qbuiaOMQf1jpbB5+j46xBcgCovo8TQMcv0bCQLKIL9w==}
cpu: [x64]
os: [freebsd]
'@yuku-codegen/binding-linux-arm-gnu@0.10.2':
resolution: {integrity: sha512-xJDpYAsKV5+eaiBhTYl05fvT6sst4PsCeuIFMRu9b0/WCSrNQPfCDtAQ5/MS6xNpsdujdZEPR+gmfWk3ZG5i3A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@yuku-codegen/binding-linux-arm-musl@0.10.2':
resolution: {integrity: sha512-qfTkgd7AEx61l4K9VtXWCGTxc/fmXJ4ZheDz3i+/AAJUtg4sa0bPrc0VBxPggB/rayR8Z3+JfrBItuyanBJJ9Q==}
cpu: [arm]
os: [linux]
libc: [musl]
'@yuku-codegen/binding-linux-arm64-gnu@0.10.2':
resolution: {integrity: sha512-4e6Mifm/4UdjtU3D8mATIrvgP+xEiH8xtQOjd0zpd72XKWT7ug3sdyhq2utkkZFP6BvYp7SgZrI+aR4VeIOHdw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@yuku-codegen/binding-linux-arm64-musl@0.10.2':
resolution: {integrity: sha512-RdsJrUfDYFVV3JOmWFUWwSu31fa1APn12xDeIKxgl/YWxrabwtxsDBNjF2561c7tmcbiewdCUvngqRs8AyGVLA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@yuku-codegen/binding-linux-x64-gnu@0.10.2':
resolution: {integrity: sha512-mVzWimEPPreaPyXIUvxsHoJzvO7ckZ5j0LgQFHz04zQIRwt9A9T2hA7K5/jYjJKkMxWG/U2VjhGNwVhtyU+uqg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@yuku-codegen/binding-linux-x64-musl@0.10.2':
resolution: {integrity: sha512-Y77fyISurmr2mvO1yeq3u+x/WJbACgPR4w+lzEVx30ViCxZzIGrZPRN1yEdZ9xUNPNsIO5thBHIdRNG+UGUG+Q==}
cpu: [x64]
os: [linux]
libc: [musl]
'@yuku-codegen/binding-win32-arm64@0.10.2':
resolution: {integrity: sha512-1+tGLyG0u5YYy0lev4ck7/0sd8xJ9SZde0dLut7qLDPZV5WVTV8x6OxNXUne/PGuHZyO1vK+txPQzzHOyXHGSw==}
cpu: [arm64]
os: [win32]
'@yuku-codegen/binding-win32-x64@0.10.2':
resolution: {integrity: sha512-8a2SExRohRbwC0MY4VpOF0/RSckrJ2ASZqAor5/RYSJJaeadR93n10gZzcKT6pY9ukiSOIZCRKVRD2tH73T0qA==}
cpu: [x64]
os: [win32]
'@yuku-parser/binding-android-arm64@0.10.2':
resolution: {integrity: sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw==}
cpu: [arm64]
os: [android]
'@yuku-parser/binding-darwin-arm64@0.10.2':
resolution: {integrity: sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA==}
cpu: [arm64]
os: [darwin]
'@yuku-parser/binding-darwin-x64@0.10.2':
resolution: {integrity: sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ==}
cpu: [x64]
os: [darwin]
'@yuku-parser/binding-freebsd-x64@0.10.2':
resolution: {integrity: sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ==}
cpu: [x64]
os: [freebsd]
'@yuku-parser/binding-linux-arm-gnu@0.10.2':
resolution: {integrity: sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@yuku-parser/binding-linux-arm-musl@0.10.2':
resolution: {integrity: sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@yuku-parser/binding-linux-arm64-gnu@0.10.2':
resolution: {integrity: sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@yuku-parser/binding-linux-arm64-musl@0.10.2':
resolution: {integrity: sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@yuku-parser/binding-linux-x64-gnu@0.10.2':
resolution: {integrity: sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@yuku-parser/binding-linux-x64-musl@0.10.2':
resolution: {integrity: sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w==}
cpu: [x64]
os: [linux]
libc: [musl]
'@yuku-parser/binding-win32-arm64@0.10.2':
resolution: {integrity: sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw==}
cpu: [arm64]
os: [win32]
'@yuku-parser/binding-win32-x64@0.10.2':
resolution: {integrity: sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA==}
cpu: [x64]
os: [win32]
'@yuku-toolchain/types@0.10.2':
resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
cac@7.0.0:
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
engines: {node: '>=20.19.0'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -630,13 +774,29 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
dts-resolver@3.0.0:
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
engines: {node: ^22.18.0 || >=24.0.0}
peerDependencies:
oxc-resolver: '>=11.0.0'
peerDependenciesMeta:
oxc-resolver:
optional: true
effect@4.0.0-rc.115:
resolution: {integrity: sha512-ogYulZ5ffeOzrJqQrG0XOkDO5lKn2s9KNHhoxJy/6wKUkF0i12dlVXWA9TgsgQ+zV/JzizyG0+XMPepZEQX6vw==}
empathic@2.0.1:
resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
engines: {node: '>=14'}
es-module-lexer@2.3.2:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
@@ -661,6 +821,17 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-tsconfig@5.0.0-beta.6:
resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==}
engines: {node: '>=20.20.0'}
hookable@6.1.2:
resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==}
import-without-cache@0.4.1:
resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==}
engines: {node: ^22.18.0 || >=24.0.0}
lightningcss-android-arm64@1.33.0:
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
engines: {node: '>= 12.0.0'}
@@ -747,6 +918,10 @@ packages:
resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
engines: {node: '>=12.20.0'}
obug@3.0.0:
resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
engines: {node: '>=12.20.0'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -758,10 +933,38 @@ packages:
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
engines: {node: ^10 || ^12 || >=14}
quansync@1.0.0:
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
redis@6.2.1:
resolution: {integrity: sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg==}
engines: {node: '>= 20.0.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
rolldown-plugin-dts@0.28.6:
resolution: {integrity: sha512-qKrFtBfRfR2hP233m7Ic9zf3wv6MSYdZghWKMdEO6dRYZGlNfINJAa1P5ZVvyHh3mrcd7IJQvIY8L9vnm+v5rQ==}
engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
peerDependencies:
'@typescript/native-preview': '*'
'@volar/typescript': ~2.4.0
'@vue/language-core': ~3.2.0 || ~3.3.0
rolldown: ^1.2.0
typescript: ^5.0.0 || ^6.0.0 || ~7.0.0
vue-tsc: ~3.2.0 || ~3.3.0
peerDependenciesMeta:
'@typescript/native-preview':
optional: true
'@volar/typescript':
optional: true
'@vue/language-core':
optional: true
typescript:
optional: true
vue-tsc:
optional: true
rolldown@1.2.9:
resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -788,15 +991,60 @@ packages:
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
engines: {node: '>=18'}
tinyexec@1.3.1:
resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
engines: {node: '>=18'}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
tsdown@0.23.0:
resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==}
engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0}
hasBin: true
peerDependencies:
'@arethetypeswrong/core': ^0.18.1
'@tsdown/css': 0.23.0
'@tsdown/exe': 0.23.0
'@vitejs/devtools': '*'
publint: ^0.3.8
tsx: '*'
typescript: ^5.0.0 || ^6.0.0 || ^7.0.0
unplugin-unused: '>=0.5.0'
unrun: '*'
peerDependenciesMeta:
'@arethetypeswrong/core':
optional: true
'@tsdown/css':
optional: true
'@tsdown/exe':
optional: true
'@vitejs/devtools':
optional: true
publint:
optional: true
tsx:
optional: true
typescript:
optional: true
unplugin-unused:
optional: true
unrun:
optional: true
typescript@7.0.2:
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
engines: {node: '>=16.20.0'}
hasBin: true
unconfig-core@7.5.0:
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
undici-types@8.9.0:
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
@@ -804,6 +1052,10 @@ packages:
resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==}
engines: {node: '>=22.19.0'}
verkit@0.4.0:
resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==}
engines: {node: '>=18.12.0'}
vite@8.3.0:
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -925,6 +1177,15 @@ packages:
utf-8-validate:
optional: true
yuku-ast@0.10.2:
resolution: {integrity: sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ==}
yuku-codegen@0.10.2:
resolution: {integrity: sha512-hentl2dtrF6cPjiAirtkfxfFZBA6Hdm61UqRJ7cA0qrlDNMk9PZGOjw5w1mx3E0hu/6pHcJF4dJW+D0SXHPZOA==}
yuku-parser@0.10.2:
resolution: {integrity: sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A==}
snapshots:
'@biomejs/biome@2.5.14':
@@ -1028,6 +1289,10 @@ snapshots:
'@oxc-project/types@0.150.0': {}
'@quansync/fs@1.1.0':
dependencies:
quansync: 1.0.0
'@redis/bloom@6.2.1(@redis/client@6.2.1)':
dependencies:
'@redis/client': 6.2.1
@@ -1183,16 +1448,98 @@ snapshots:
'@vitest/spy@5.0.1': {}
'@yuku-codegen/binding-android-arm64@0.10.2':
optional: true
'@yuku-codegen/binding-darwin-arm64@0.10.2':
optional: true
'@yuku-codegen/binding-darwin-x64@0.10.2':
optional: true
'@yuku-codegen/binding-freebsd-x64@0.10.2':
optional: true
'@yuku-codegen/binding-linux-arm-gnu@0.10.2':
optional: true
'@yuku-codegen/binding-linux-arm-musl@0.10.2':
optional: true
'@yuku-codegen/binding-linux-arm64-gnu@0.10.2':
optional: true
'@yuku-codegen/binding-linux-arm64-musl@0.10.2':
optional: true
'@yuku-codegen/binding-linux-x64-gnu@0.10.2':
optional: true
'@yuku-codegen/binding-linux-x64-musl@0.10.2':
optional: true
'@yuku-codegen/binding-win32-arm64@0.10.2':
optional: true
'@yuku-codegen/binding-win32-x64@0.10.2':
optional: true
'@yuku-parser/binding-android-arm64@0.10.2':
optional: true
'@yuku-parser/binding-darwin-arm64@0.10.2':
optional: true
'@yuku-parser/binding-darwin-x64@0.10.2':
optional: true
'@yuku-parser/binding-freebsd-x64@0.10.2':
optional: true
'@yuku-parser/binding-linux-arm-gnu@0.10.2':
optional: true
'@yuku-parser/binding-linux-arm-musl@0.10.2':
optional: true
'@yuku-parser/binding-linux-arm64-gnu@0.10.2':
optional: true
'@yuku-parser/binding-linux-arm64-musl@0.10.2':
optional: true
'@yuku-parser/binding-linux-x64-gnu@0.10.2':
optional: true
'@yuku-parser/binding-linux-x64-musl@0.10.2':
optional: true
'@yuku-parser/binding-win32-arm64@0.10.2':
optional: true
'@yuku-parser/binding-win32-x64@0.10.2':
optional: true
'@yuku-toolchain/types@0.10.2': {}
assertion-error@2.0.1: {}
cac@7.0.0: {}
chai@6.2.2: {}
cluster-key-slot@1.1.2: {}
defu@6.1.7: {}
detect-libc@2.1.2: {}
dts-resolver@3.0.0: {}
effect@4.0.0-rc.115: {}
empathic@2.0.1: {}
es-module-lexer@2.3.2: {}
estree-walker@3.0.3:
@@ -1208,6 +1555,14 @@ snapshots:
fsevents@2.3.3:
optional: true
get-tsconfig@5.0.0-beta.6:
dependencies:
resolve-pkg-maps: 1.0.0
hookable@6.1.2: {}
import-without-cache@0.4.1: {}
lightningcss-android-arm64@1.33.0:
optional: true
@@ -1265,6 +1620,8 @@ snapshots:
obug@2.2.1: {}
obug@3.0.0: {}
picocolors@1.1.1: {}
picomatch@4.0.7: {}
@@ -1275,6 +1632,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
quansync@1.0.0: {}
redis@6.2.1:
dependencies:
'@redis/bloom': 6.2.1(@redis/client@6.2.1)
@@ -1286,6 +1645,22 @@ snapshots:
- '@node-rs/xxhash'
- '@opentelemetry/api'
resolve-pkg-maps@1.0.0: {}
rolldown-plugin-dts@0.28.6(rolldown@1.2.9)(typescript@7.0.2):
dependencies:
dts-resolver: 3.0.0
get-tsconfig: 5.0.0-beta.6
obug: 3.0.0
rolldown: 1.2.9
yuku-ast: 0.10.2
yuku-codegen: 0.10.2
yuku-parser: 0.10.2
optionalDependencies:
typescript: 7.0.2
transitivePeerDependencies:
- oxc-resolver
rolldown@1.2.9:
dependencies:
'@oxc-project/types': 0.150.0
@@ -1319,11 +1694,40 @@ snapshots:
tinyexec@1.3.0: {}
tinyexec@1.3.1: {}
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.7)
picomatch: 4.0.7
tree-kill@1.2.2: {}
tsdown@0.23.0(typescript@7.0.2):
dependencies:
cac: 7.0.0
defu: 6.1.7
empathic: 2.0.1
hookable: 6.1.2
import-without-cache: 0.4.1
obug: 2.2.1
picomatch: 4.0.7
rolldown: 1.2.9
rolldown-plugin-dts: 0.28.6(rolldown@1.2.9)(typescript@7.0.2)
tinyexec: 1.3.1
tinyglobby: 0.2.17
tree-kill: 1.2.2
unconfig-core: 7.5.0
verkit: 0.4.0
optionalDependencies:
typescript: 7.0.2
transitivePeerDependencies:
- '@typescript/native-preview'
- '@volar/typescript'
- '@vue/language-core'
- oxc-resolver
- vue-tsc
typescript@7.0.2:
optionalDependencies:
'@typescript/typescript-aix-ppc64': 7.0.2
@@ -1347,10 +1751,17 @@ snapshots:
'@typescript/typescript-win32-arm64': 7.0.2
'@typescript/typescript-win32-x64': 7.0.2
unconfig-core@7.5.0:
dependencies:
'@quansync/fs': 1.1.0
quansync: 1.0.0
undici-types@8.9.0: {}
undici@8.10.2: {}
verkit@0.4.0: {}
vite@8.3.0(@types/node@26.6.1):
dependencies:
lightningcss: 1.33.0
@@ -1406,3 +1817,42 @@ snapshots:
stackback: 0.0.2
ws@8.21.3: {}
yuku-ast@0.10.2:
dependencies:
'@yuku-toolchain/types': 0.10.2
yuku-codegen@0.10.2:
dependencies:
'@yuku-toolchain/types': 0.10.2
optionalDependencies:
'@yuku-codegen/binding-android-arm64': 0.10.2
'@yuku-codegen/binding-darwin-arm64': 0.10.2
'@yuku-codegen/binding-darwin-x64': 0.10.2
'@yuku-codegen/binding-freebsd-x64': 0.10.2
'@yuku-codegen/binding-linux-arm-gnu': 0.10.2
'@yuku-codegen/binding-linux-arm-musl': 0.10.2
'@yuku-codegen/binding-linux-arm64-gnu': 0.10.2
'@yuku-codegen/binding-linux-arm64-musl': 0.10.2
'@yuku-codegen/binding-linux-x64-gnu': 0.10.2
'@yuku-codegen/binding-linux-x64-musl': 0.10.2
'@yuku-codegen/binding-win32-arm64': 0.10.2
'@yuku-codegen/binding-win32-x64': 0.10.2
yuku-parser@0.10.2:
dependencies:
'@yuku-toolchain/types': 0.10.2
yuku-ast: 0.10.2
optionalDependencies:
'@yuku-parser/binding-android-arm64': 0.10.2
'@yuku-parser/binding-darwin-arm64': 0.10.2
'@yuku-parser/binding-darwin-x64': 0.10.2
'@yuku-parser/binding-freebsd-x64': 0.10.2
'@yuku-parser/binding-linux-arm-gnu': 0.10.2
'@yuku-parser/binding-linux-arm-musl': 0.10.2
'@yuku-parser/binding-linux-arm64-gnu': 0.10.2
'@yuku-parser/binding-linux-arm64-musl': 0.10.2
'@yuku-parser/binding-linux-x64-gnu': 0.10.2
'@yuku-parser/binding-linux-x64-musl': 0.10.2
'@yuku-parser/binding-win32-arm64': 0.10.2
'@yuku-parser/binding-win32-x64': 0.10.2
+3 -3
View File
@@ -5,8 +5,8 @@
"ref": "effect@4.0.0-rc.115"
},
{
"url": "https://github.com/microsoft/language-server-protocol",
"name": "lsp",
"ref": "gh-pages"
"url": "https://github.com/microsoft/language-server-protocol",
"name": "lsp",
"ref": "gh-pages"
}
]
+1 -1
View File
@@ -28,7 +28,7 @@ export interface Options {
const info = {
name: "lsp-mcp",
version: "0.1.0",
version: "0.1.2",
protocols: [
McpProtocol.v2025_11_25,
McpProtocol.v2025_06_18,
+355 -128
View File
@@ -5,12 +5,20 @@
* `request` waits for the server to become idle (no `$/progress` work in
* flight) and retries transient `ContentModified` / `ServerCancelled`
* failures, so callers can treat the server as if it were always ready.
*
* The process is supervised: when it exits unexpectedly it is started again
* according to the restart policy, the documents that were open are opened in
* the new process, and requests caught by the crash wait for it and retry.
* Once the policy gives up, every request fails with an `LspError` saying so.
*/
import { spawn } from "node:child_process";
import { PassThrough } from "node:stream";
import { NodeStream } from "@effect/platform-node";
import {
Cause,
Context,
Deferred,
Duration,
Effect,
FiberSet,
HashSet,
@@ -25,14 +33,23 @@ import {
import {
CancellationTokenSource,
type ClientCapabilities,
type CompletionItemKind,
type ConfigurationParams,
ConfigurationRequest,
createMessageConnection,
DidChangeConfigurationNotification,
DidChangeTextDocumentNotification,
type DidChangeTextDocumentParams,
DidCloseTextDocumentNotification,
type DidCloseTextDocumentParams,
DidOpenTextDocumentNotification,
type DidOpenTextDocumentParams,
ErrorCodes,
ExitNotification,
type HandlerResult,
InitializedNotification,
InitializeRequest,
type InitializeResult,
LogMessageNotification,
type LSPAny,
LSPErrorCodes,
@@ -46,12 +63,21 @@ import {
ShutdownRequest,
StreamMessageReader,
StreamMessageWriter,
type SymbolKind,
type TextDocumentItem,
UnregistrationRequest,
WorkDoneProgressCreateRequest,
WorkspaceFoldersRequest,
} from "vscode-languageserver-protocol/node";
import { URI } from "vscode-uri";
/**
* What to do when the server exits unexpectedly: never start it again, always
* start it again, or start it again but give up after this many crashes in a
* row (less than a minute apart).
*/
export type Restart = "never" | "always" | number;
export interface Options {
readonly command: string;
readonly args: ReadonlyArray<string>;
@@ -60,18 +86,26 @@ export interface Options {
readonly initializationOptions?: unknown;
/** Served as `workspace/configuration` and pushed via `didChangeConfiguration`. */
readonly settings?: unknown;
/** Defaults to giving up after 3 crashes in a row. */
readonly restart?: Restart;
}
/** Failures worth retrying: the server was busy, or it went away and may come back. */
const transient: ReadonlySet<number> = new Set([
LSPErrorCodes.ContentModified,
LSPErrorCodes.ServerCancelled,
ErrorCodes.ConnectionInactive,
ErrorCodes.PendingResponseRejected,
ErrorCodes.MessageWriteError,
]);
export class LspError extends Schema.TaggedError<LspError>()("LspError", {
method: Schema.String,
message: Schema.String,
code: Schema.optional(Schema.Int),
}) {
get retryable() {
return (
this.code === LSPErrorCodes.ContentModified ||
this.code === LSPErrorCodes.ServerCancelled
);
return this.code !== undefined && transient.has(this.code);
}
}
@@ -103,7 +137,7 @@ export class LanguageServer extends Context.Service<
type: RequestType<P, R, unknown>,
handler: (params: P) => Effect.Effect<R, unknown>,
) => Effect.Effect<void, never, Scope.Scope>;
/** `window/logMessage` and `window/showMessage` traffic. */
/** `window/logMessage` and `window/showMessage` traffic, plus restart announcements. */
readonly logs: Stream.Stream<LogMessage>;
}
>()("lsp-mcp/lsp/LanguageServer") {
@@ -111,88 +145,64 @@ export class LanguageServer extends Context.Service<
Layer.effect(LanguageServer, make(options));
}
/** One process and its connection; replaced on crash. */
interface Session {
readonly initialized: InitializeResult;
readonly exited: Deferred.Deferred<never, LspError>;
readonly send: <P, R>(
type: RequestType<P, R, unknown>,
params: P,
) => Effect.Effect<R, LspError>;
readonly notify: <P>(
type: NotificationType<P>,
params: P,
) => Effect.Effect<void, LspError>;
}
const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
const run = yield* FiberSet.makeRuntime<never>();
const runPromise = yield* FiberSet.makeRuntimePromise<never>();
const exited = yield* Deferred.make<never, LspError>();
const events = yield* PubSub.unbounded<{
readonly method: string;
readonly params: unknown;
}>();
const lifecycle = yield* PubSub.unbounded<LogMessage>();
const progress = yield* SubscriptionRef.make(
HashSet.empty<string | number>(),
);
const exit = (message: string) =>
Deferred.doneUnsafe(
exited,
Effect.fail(new LspError({ method: "connection", message })),
);
const initialized = yield* Deferred.make<InitializeResult, LspError>();
/** The live session, or the one being started; failed once the policy gives up. */
let current = Deferred.makeUnsafe<Session, LspError>();
const session = Effect.suspend(() => Deferred.await(current));
/** What the server has been told is open, to open it again after a restart. */
const documents = new Map<string, TextDocumentItem>();
const limit =
options.restart === "never"
? 0
: options.restart === "always"
? Number.POSITIVE_INFINITY
: (options.restart ?? 3);
const child = yield* Effect.acquireRelease(
Effect.sync(() =>
spawn(options.command, options.args, {
cwd: options.root,
stdio: "pipe",
}),
),
(child) =>
Effect.sync(() => void (child.exitCode === null && child.kill())),
);
child.on("error", (error) =>
exit(`failed to start ${options.command}: ${error.message}`),
);
child.on("exit", (code, signal) =>
exit(`${options.command} exited (${signal ?? code})`),
);
yield* NodeStream.fromReadable<Uint8Array, never>({
evaluate: () => child.stderr,
onError: () => undefined as never,
}).pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => Effect.logDebug(line)),
Effect.annotateLogs("source", `${options.command}:stderr`),
Effect.forkScoped,
);
const connection = createMessageConnection(
new StreamMessageReader(child.stdout),
new StreamMessageWriter(child.stdin),
{
error: (m) => run(Effect.logError(m)),
warn: (m) => run(Effect.logWarning(m)),
info: (m) => run(Effect.logInfo(m)),
log: (m) => run(Effect.logDebug(m)),
},
);
yield* Effect.addFinalizer(() => Effect.sync(() => connection.dispose()));
connection.onClose(() => exit(`${options.command} closed the connection`));
connection.onNotification((method, params) => {
PubSub.publishUnsafe(events, { method, params });
});
connection.onUnhandledProgress(({ token, value }) =>
run(
SubscriptionRef.update(progress, (active) =>
value.kind === "begin"
? HashSet.add(active, token)
: value.kind === "end"
? HashSet.remove(active, token)
: active,
),
),
);
connection.onRequest(WorkDoneProgressCreateRequest.type, () => undefined);
connection.onRequest(RegistrationRequest.type, () => undefined);
connection.onRequest(UnregistrationRequest.type, () => undefined);
connection.onRequest(ConfigurationRequest.type, ({ items }) =>
items.map((item) => section(options.settings, item.section)),
);
const rootUri = URI.file(options.root).toString();
const workspaceFolders = [
{ uri: rootUri, name: options.root.split(/[\\/]/).pop() ?? options.root },
];
connection.onRequest(WorkspaceFoldersRequest.type, () => workspaceFolders);
connection.listen();
const handlers = new Map<
string,
(params: unknown) => HandlerResult<unknown, unknown>
>([
[WorkDoneProgressCreateRequest.method, () => undefined],
[RegistrationRequest.method, () => undefined],
[UnregistrationRequest.method, () => undefined],
[
ConfigurationRequest.method,
(params) =>
(params as ConfigurationParams).items.map((item) =>
section(options.settings, item.section),
),
],
[WorkspaceFoldersRequest.method, () => workspaceFolders],
]);
const failure = (method: string, error: unknown) =>
error instanceof ResponseError
@@ -202,19 +212,204 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
message: error instanceof Error ? error.message : String(error),
});
const send = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
Effect.callback<R, LspError>((resume) => {
const cancellation = new CancellationTokenSource();
try {
connection.sendRequest(type, params as never, cancellation.token).then(
(result) => resume(Effect.succeed(result)),
(error) => resume(Effect.fail(failure(type.method, error))),
);
} catch (error) {
resume(Effect.fail(failure(type.method, error)));
}
return Effect.ignore(Effect.try(() => cancellation.cancel()));
}).pipe(Effect.raceFirst(Deferred.await(exited)));
const announce = (level: LogLevel, message: string) =>
Effect.andThen(
level === "error" ? Effect.logError(message) : Effect.logWarning(message),
PubSub.publish(lifecycle, { level, message }),
);
const start = Effect.fn("LanguageServer.start")(function* () {
const exited = yield* Deferred.make<never, LspError>();
const exit = (message: string) => {
if (Deferred.isDoneUnsafe(exited)) return;
// Whoever was using this session now waits for the next one; swap before
// signalling, as waiters resume synchronously and may give up right away.
if (Deferred.isDoneUnsafe(current)) current = Deferred.makeUnsafe();
Deferred.doneUnsafe(
exited,
Effect.fail(
new LspError({
method: "connection",
message,
code: ErrorCodes.ConnectionInactive,
}),
),
);
};
const child = yield* Effect.acquireRelease(
Effect.sync(() =>
spawn(options.command, options.args, {
cwd: options.root,
stdio: "pipe",
}),
),
(child) =>
Effect.sync(() => void (child.exitCode === null && child.kill())),
);
child.on("error", (error) =>
exit(`failed to start ${options.command}: ${error.message}`),
);
child.on("exit", (code, signal) =>
exit(`${options.command} exited (${signal ?? code})`),
);
yield* NodeStream.fromReadable<Uint8Array, never>({
evaluate: () => child.stderr,
onError: () => undefined as never,
}).pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => Effect.logDebug(line)),
Effect.annotateLogs("source", `${options.command}:stderr`),
Effect.forkScoped,
);
// Writes go through a buffer: a broken pipe must end the session, not
// surface as a write failure (vscode-jsonrpc leaks those as unhandled rejections).
const input = new PassThrough();
input.pipe(child.stdin);
child.stdin.on("error", (error) =>
exit(`${options.command}: ${error.message}`),
);
const connection = createMessageConnection(
new StreamMessageReader(child.stdout),
new StreamMessageWriter(input),
{
error: (m) => run(Effect.logError(m)),
warn: (m) => run(Effect.logWarning(m)),
info: (m) => run(Effect.logInfo(m)),
log: (m) => run(Effect.logDebug(m)),
},
);
yield* Effect.addFinalizer(() => Effect.sync(() => connection.dispose()));
connection.onClose(() => exit(`${options.command} closed the connection`));
connection.onNotification((method, params) => {
PubSub.publishUnsafe(events, { method, params });
});
connection.onUnhandledProgress(({ token, value }) =>
run(
SubscriptionRef.update(progress, (active) =>
value.kind === "begin"
? HashSet.add(active, token)
: value.kind === "end"
? HashSet.remove(active, token)
: active,
),
),
);
connection.onRequest(
(method, params) =>
handlers.get(method)?.(params) ??
new ResponseError(ErrorCodes.MethodNotFound, `unhandled ${method}`),
);
connection.listen();
const send = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
Effect.callback<R, LspError>((resume) => {
const cancellation = new CancellationTokenSource();
try {
connection
.sendRequest(type, params as never, cancellation.token)
.then(
(result) => resume(Effect.succeed(result)),
(error) => resume(Effect.fail(failure(type.method, error))),
);
} catch (error) {
resume(Effect.fail(failure(type.method, error)));
}
return Effect.ignore(Effect.try(() => cancellation.cancel()));
}).pipe(Effect.raceFirst(Deferred.await(exited)));
const notify = <P>(type: NotificationType<P>, params: P) =>
Effect.tryPromise({
try: () => connection.sendNotification(type, params as never),
catch: (error) => failure(type.method, error),
});
yield* SubscriptionRef.set(progress, HashSet.empty());
const initialized = yield* send(InitializeRequest.type, {
processId: process.pid,
clientInfo: { name: "lsp-mcp", version: "0.1.2" },
locale: "en",
rootPath: options.root,
rootUri,
workspaceFolders,
capabilities: clientCapabilities,
initializationOptions: options.initializationOptions as LSPAny,
});
yield* notify(InitializedNotification.type, {});
if (options.settings !== undefined) {
yield* notify(DidChangeConfigurationNotification.type, {
settings: options.settings as LSPAny,
});
}
yield* Effect.forEach(
documents.values(),
(textDocument) =>
notify(DidOpenTextDocumentNotification.type, { textDocument }),
{ discard: true },
);
const shutdown = Effect.tryPromise(() =>
connection.sendRequest(ShutdownRequest.type),
).pipe(
Effect.timeout("3 seconds"),
Effect.andThen(
Effect.tryPromise(() =>
connection.sendNotification(ExitNotification.type),
),
),
Effect.andThen(Deferred.await(exited).pipe(Effect.timeout("2 seconds"))),
Effect.ignore,
);
yield* Effect.addFinalizer(() =>
Effect.suspend(() =>
Deferred.isDoneUnsafe(exited) ? Effect.void : shutdown,
),
);
return { initialized, exited, send, notify } satisfies Session;
});
/** Runs one session until its process exits, and fails with the reason. */
const serve = Effect.scoped(
Effect.gen(function* () {
const session = yield* start().pipe(
Effect.tapError((error) => Deferred.fail(initialized, error)),
);
yield* Deferred.succeed(current, session);
yield* Deferred.succeed(initialized, session.initialized);
return yield* Deferred.await(session.exited);
}),
);
const policy = restarts(limit).pipe(
// Only crashes are worth retrying; a server that fails to initialize is broken.
Schedule.while(({ input }) => input.code === ErrorCodes.ConnectionInactive),
Schedule.tap(({ input, output, duration }) =>
announce(
"warning",
`${input.message}; restarting in ${Duration.format(duration)}${
Number.isFinite(limit) ? ` (crash ${output} of ${limit})` : ""
}`,
),
),
);
const giveUp = (error: LspError) => {
const reason =
error.code !== ErrorCodes.ConnectionInactive
? `restarting ${options.command} failed (${error.method}: ${error.message})`
: limit === 0
? `${error.message}; restarting is disabled`
: `${error.message}; that is ${limit} crash${limit === 1 ? "" : "es"} in a row`;
const message = `${reason}. Restart lsp-mcp to recover`;
return Effect.andThen(
announce("error", message),
Deferred.fail(current, new LspError({ method: "connection", message })),
);
};
yield* serve.pipe(Effect.retryOrElse(policy, giveUp), Effect.forkScoped);
const first = yield* Deferred.await(initialized);
const idle = SubscriptionRef.changes(progress).pipe(
Stream.filter(HashSet.isEmpty),
@@ -224,8 +419,9 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
);
const request = <P, R>(type: RequestType<P, R, unknown>, params: P) =>
idle.pipe(
Effect.andThen(send(type, params)),
session.pipe(
Effect.tap(() => idle),
Effect.flatMap((session) => session.send(type, params)),
Effect.retry({
while: (error) => error.retryable,
schedule: Schedule.exponential("100 millis").pipe(
@@ -235,10 +431,11 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
);
const notify = <P>(type: NotificationType<P>, params: P) =>
Effect.tryPromise({
try: () => connection.sendNotification(type, params as never),
catch: (error) => failure(type.method, error),
}).pipe(Effect.catch((error) => Effect.logWarning(error.message)));
Effect.sync(() => track(documents, type.method, params)).pipe(
Effect.andThen(session),
Effect.flatMap((session) => session.notify(type, params)),
Effect.catch((error) => Effect.logWarning(error.message)),
);
const notifications = <P>(type: NotificationType<P>): Stream.Stream<P> =>
Stream.fromPubSub(events).pipe(
@@ -252,11 +449,11 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
) =>
Effect.acquireRelease(
Effect.sync(() =>
connection.onRequest(
handlers.set(
type.method,
(params: P): HandlerResult<R, unknown> =>
(params) =>
runPromise(
handler(params).pipe(
handler(params as P).pipe(
Effect.mapError((error) =>
error instanceof ResponseError
? error
@@ -269,38 +466,9 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
) as Promise<never>,
),
),
(disposable) => Effect.sync(() => disposable.dispose()),
() => Effect.sync(() => handlers.delete(type.method)),
).pipe(Effect.asVoid);
const initialized = yield* send(InitializeRequest.type, {
processId: process.pid,
clientInfo: { name: "lsp-mcp", version: "0.1.0" },
locale: "en",
rootPath: options.root,
rootUri,
workspaceFolders,
capabilities: clientCapabilities,
initializationOptions: options.initializationOptions as LSPAny,
});
yield* notify(InitializedNotification.type, {});
if (options.settings !== undefined) {
yield* notify(DidChangeConfigurationNotification.type, {
settings: options.settings as LSPAny,
});
}
yield* Effect.addFinalizer(() =>
Effect.tryPromise(() => connection.sendRequest(ShutdownRequest.type)).pipe(
Effect.timeout("3 seconds"),
Effect.andThen(
Effect.tryPromise(() =>
connection.sendNotification(ExitNotification.type),
),
),
Effect.andThen(Deferred.await(exited).pipe(Effect.timeout("2 seconds"))),
Effect.ignore,
),
);
const logs = Stream.merge(
notifications(LogMessageNotification.type),
notifications(ShowMessageNotification.type),
@@ -311,12 +479,13 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
message,
}),
),
Stream.merge(Stream.fromPubSub(lifecycle)),
);
return LanguageServer.of({
root: options.root,
capabilities: initialized.capabilities,
serverInfo: initialized.serverInfo,
capabilities: first.capabilities,
serverInfo: first.serverInfo,
request,
notify,
notifications,
@@ -325,6 +494,61 @@ const make = Effect.fn("LanguageServer.make")(function* (options: Options) {
});
});
/** A crash this long after the previous one starts a fresh streak. */
const quiet = Duration.minutes(1);
/**
* Restart delays: exponential from 500ms, capped at 30s, giving up once
* `limit` crashes happened in a row. The output is the length of the streak.
*/
const restarts = (limit: number) =>
Schedule.fromStepWithMetadata(
Effect.sync(() => {
let streak = 0;
return ({ elapsedSincePrevious }: Schedule.InputMetadata<LspError>) => {
streak =
elapsedSincePrevious > Duration.toMillis(quiet) ? 1 : streak + 1;
return streak > limit
? Cause.done(streak)
: Effect.succeed<[number, Duration.Duration]>([
streak,
Duration.millis(Math.min(500 * 2 ** (streak - 1), 30_000)),
]);
};
}),
);
/** Keep the open-document bookkeeping in step with what we tell the server. */
const track = (
documents: Map<string, TextDocumentItem>,
method: string,
params: unknown,
) => {
switch (method) {
case DidOpenTextDocumentNotification.method: {
const { textDocument } = params as DidOpenTextDocumentParams;
documents.set(textDocument.uri, textDocument);
break;
}
case DidChangeTextDocumentNotification.method: {
const { textDocument, contentChanges } =
params as DidChangeTextDocumentParams;
const open = documents.get(textDocument.uri);
const last = contentChanges.at(-1);
if (open && last && !("range" in last)) {
documents.set(textDocument.uri, {
...open,
version: textDocument.version,
text: last.text,
});
}
break;
}
case DidCloseTextDocumentNotification.method:
documents.delete((params as DidCloseTextDocumentParams).textDocument.uri);
}
};
const logLevels: Record<MessageType, LogLevel> = {
1: "error",
2: "warning",
@@ -393,7 +617,10 @@ const clientCapabilities: ClientCapabilities = {
deprecatedSupport: true,
},
completionItemKind: {
valueSet: Array.from({ length: 25 }, (_, i) => (i + 1) as never),
valueSet: Array.from(
{ length: 25 },
(_, i) => (i + 1) as CompletionItemKind,
),
},
contextSupport: true,
},
@@ -440,5 +667,5 @@ const clientCapabilities: ClientCapabilities = {
};
function allSymbolKinds() {
return Array.from({ length: 26 }, (_, i) => (i + 1) as never);
return Array.from({ length: 26 }, (_, i) => (i + 1) as SymbolKind);
}
+23 -1
View File
@@ -10,6 +10,14 @@ import { Argument, Command, Flag } from "effect/unstable/cli";
import * as Server from "./Server.ts";
const Json = Flag.withSchema(Schema.fromJsonString(Schema.Json));
const crashes = { message: "expected a whole number of crashes" };
const Restart = Schema.Union([
Schema.Literals(["never", "always"]),
Schema.NumberFromString.check(
Schema.isInt(crashes),
Schema.isGreaterThanOrEqualTo(0, crashes),
),
]);
const command = Command.make(
"lsp-mcp",
@@ -58,6 +66,14 @@ const command = Command.make(
),
Flag.withDefault(32),
),
restart: Flag.String("restart").pipe(
Flag.withSchema(Restart),
Flag.withMetavar("never|always|N"),
Flag.withDescription(
"When the language server crashes: never start it again, always start it again, or give up after N crashes in a row (default 3)",
),
Flag.withDefault(3),
),
command: Argument.String("server").pipe(
Argument.withDescription("Language server executable"),
),
@@ -75,6 +91,7 @@ const command = Command.make(
settings,
languages,
capacity,
restart,
command,
args,
}) {
@@ -89,6 +106,7 @@ const command = Command.make(
root: path.resolve(root),
initializationOptions: Option.getOrUndefined(initOptions),
settings: Option.getOrUndefined(settings),
restart,
},
documents: { capacity, languages },
}),
@@ -111,11 +129,15 @@ const command = Command.make(
command: `lsp-mcp --settings '{"gopls":{"staticcheck":true}}' -- gopls`,
description: "Go with workspace settings",
},
{
command: "lsp-mcp --restart always -- clangd",
description: "Keep bringing the server back, however often it crashes",
},
]),
);
command.pipe(
Command.run({ version: "0.1.0" }),
Command.run({ version: "0.1.2" }),
Effect.tapCause((cause) =>
Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logError(cause),
),
+37 -1
View File
@@ -1,7 +1,7 @@
import { NodeServices } from "@effect/platform-node";
import { assert, describe, it } from "@effect/vitest";
import { Effect, FileSystem, Layer } from "effect";
import { RequestType } from "vscode-languageserver-protocol";
import { NotificationType, RequestType } from "vscode-languageserver-protocol";
import { Diagnostics } from "../src/lsp/Diagnostics.ts";
import { Documents } from "../src/lsp/Documents.ts";
import * as Editor from "../src/lsp/Editor.ts";
@@ -12,6 +12,7 @@ const State = new RequestType<null, { open: number; closes: number }, void>(
"fixture/state",
);
const Progress = new RequestType<number, string, void>("fixture/progress");
const Crash = new NotificationType<null>("fixture/crash");
const TestLayer = Layer.mergeAll(
Documents.layer({ capacity: 2, languages: {} }),
@@ -168,4 +169,39 @@ describe("Editor", () => {
);
},
);
it.layer(
Layer.mergeAll(
Documents.layer({ capacity: 2, languages: {} }),
Diagnostics.layer,
).pipe(
Layer.provideMerge(
workspace({ "a.ts": "const alpha = 1\n" }, { restart: 1 }),
),
Layer.provideMerge(NodeServices.layer),
),
{ excludeTestServices: true },
)("when the server crashes", (it) => {
it.effect(
"starts it again with the same documents open, until the policy gives up",
() =>
Effect.gen(function* () {
const lsp = yield* LanguageServer;
const hover = Editor.hover({
file: "a.ts",
line: 1,
symbol: "alpha",
});
yield* hover;
yield* lsp.notify(Crash, null);
assert.strictEqual((yield* hover)?.contents, "word **alpha**");
assert.strictEqual((yield* lsp.request(State, null)).open, 1);
yield* lsp.notify(Crash, null);
const error = yield* Effect.flip(hover);
assert.match(error.message, /1 crash in a row/);
}),
);
});
});
+3 -1
View File
@@ -2,7 +2,8 @@
* A miniature language server used by the tests. Words are symbols: hover
* describes the word under the cursor, definition is its first occurrence,
* references and rename cover every occurrence, and each line containing
* "TODO" yields a warning. `fixture/state` reports open/close counts.
* "TODO" yields a warning. `fixture/state` reports open/close counts and
* `fixture/crash` kills the process.
*/
import {
createConnection,
@@ -136,6 +137,7 @@ connection.onDocumentSymbol(({ textDocument }) => {
);
});
connection.onNotification("fixture/crash", () => process.exit(1));
connection.onRequest("fixture/state", () => ({
open: documents.all().length,
closes,
+6 -2
View File
@@ -1,11 +1,14 @@
import { fileURLToPath } from "node:url";
import { Effect, FileSystem, Layer, Path } from "effect";
import { LanguageServer } from "../../src/lsp/LanguageServer.ts";
import { LanguageServer, type Options } from "../../src/lsp/LanguageServer.ts";
const server = fileURLToPath(new URL("./server.ts", import.meta.url));
/** A temporary workspace with the given files, served by the fixture language server. */
export const workspace = (files: Record<string, string>) =>
export const workspace = (
files: Record<string, string>,
options?: Pick<Options, "restart">,
) =>
Layer.unwrap(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
@@ -18,6 +21,7 @@ export const workspace = (files: Record<string, string>) =>
command: process.execPath,
args: [server],
root,
...options,
});
}),
);
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: "src/main.ts",
format: "esm",
outDir: "dist",
deps: {
alwaysBundle: ["*"],
},
treeshake: true,
minify: true,
sourcemap: false,
});