@wavexzore/sandbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/Dockerfile +14 -0
  2. package/LICENSE +661 -0
  3. package/NOTICE +3 -0
  4. package/README.md +153 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.js +9 -0
  7. package/dist/sandbox/cli/install.d.ts +5 -0
  8. package/dist/sandbox/cli/install.js +335 -0
  9. package/dist/sandbox/cli/local-store.d.ts +87 -0
  10. package/dist/sandbox/cli/local-store.js +604 -0
  11. package/dist/sandbox/cli/opencode-config.d.ts +25 -0
  12. package/dist/sandbox/cli/opencode-config.js +240 -0
  13. package/dist/sandbox/cli/path.d.ts +64 -0
  14. package/dist/sandbox/cli/path.js +127 -0
  15. package/dist/sandbox/cli/types.d.ts +145 -0
  16. package/dist/sandbox/cli/types.js +6 -0
  17. package/dist/sandbox/cli/wavexzore-sandbox.d.ts +65 -0
  18. package/dist/sandbox/cli/wavexzore-sandbox.js +577 -0
  19. package/dist/sandbox/core/cli-helper.d.ts +19 -0
  20. package/dist/sandbox/core/cli-helper.js +64 -0
  21. package/dist/sandbox/core/docker-archive-utils.d.ts +3 -0
  22. package/dist/sandbox/core/docker-archive-utils.js +50 -0
  23. package/dist/sandbox/core/docker-sandbox.d.ts +51 -0
  24. package/dist/sandbox/core/docker-sandbox.js +675 -0
  25. package/dist/sandbox/core/edit/filediff.d.ts +16 -0
  26. package/dist/sandbox/core/edit/filediff.js +21 -0
  27. package/dist/sandbox/core/edit/index.d.ts +5 -0
  28. package/dist/sandbox/core/edit/index.js +5 -0
  29. package/dist/sandbox/core/edit/line-endings.d.ts +4 -0
  30. package/dist/sandbox/core/edit/line-endings.js +10 -0
  31. package/dist/sandbox/core/edit/lock.d.ts +1 -0
  32. package/dist/sandbox/core/edit/lock.js +18 -0
  33. package/dist/sandbox/core/edit/replace.d.ts +10 -0
  34. package/dist/sandbox/core/edit/replace.js +14 -0
  35. package/dist/sandbox/core/edit/replacers.d.ts +15 -0
  36. package/dist/sandbox/core/edit/replacers.js +241 -0
  37. package/dist/sandbox/core/logger.d.ts +15 -0
  38. package/dist/sandbox/core/logger.js +59 -0
  39. package/dist/sandbox/core/lsp/client.d.ts +63 -0
  40. package/dist/sandbox/core/lsp/client.js +533 -0
  41. package/dist/sandbox/core/lsp/config.d.ts +13 -0
  42. package/dist/sandbox/core/lsp/config.js +36 -0
  43. package/dist/sandbox/core/lsp/diagnostics.d.ts +12 -0
  44. package/dist/sandbox/core/lsp/diagnostics.js +65 -0
  45. package/dist/sandbox/core/lsp/index.d.ts +4 -0
  46. package/dist/sandbox/core/lsp/index.js +4 -0
  47. package/dist/sandbox/core/lsp/language.d.ts +24 -0
  48. package/dist/sandbox/core/lsp/language.js +249 -0
  49. package/dist/sandbox/core/lsp/manager.d.ts +77 -0
  50. package/dist/sandbox/core/lsp/manager.js +237 -0
  51. package/dist/sandbox/core/lsp/tooling.d.ts +14 -0
  52. package/dist/sandbox/core/lsp/tooling.js +78 -0
  53. package/dist/sandbox/core/patch-parser.d.ts +23 -0
  54. package/dist/sandbox/core/patch-parser.js +248 -0
  55. package/dist/sandbox/core/path-map.d.ts +9 -0
  56. package/dist/sandbox/core/path-map.js +73 -0
  57. package/dist/sandbox/core/project-data-storage.d.ts +42 -0
  58. package/dist/sandbox/core/project-data-storage.js +167 -0
  59. package/dist/sandbox/core/read/binary.d.ts +4 -0
  60. package/dist/sandbox/core/read/binary.js +80 -0
  61. package/dist/sandbox/core/read/format.d.ts +38 -0
  62. package/dist/sandbox/core/read/format.js +85 -0
  63. package/dist/sandbox/core/read/index.d.ts +3 -0
  64. package/dist/sandbox/core/read/index.js +3 -0
  65. package/dist/sandbox/core/read/permissions.d.ts +7 -0
  66. package/dist/sandbox/core/read/permissions.js +13 -0
  67. package/dist/sandbox/core/session-manager.d.ts +29 -0
  68. package/dist/sandbox/core/session-manager.js +338 -0
  69. package/dist/sandbox/core/shell/config.d.ts +7 -0
  70. package/dist/sandbox/core/shell/config.js +82 -0
  71. package/dist/sandbox/core/shell/output.d.ts +35 -0
  72. package/dist/sandbox/core/shell/output.js +80 -0
  73. package/dist/sandbox/core/shell/parser.d.ts +7 -0
  74. package/dist/sandbox/core/shell/parser.js +122 -0
  75. package/dist/sandbox/core/shell/permissions.d.ts +13 -0
  76. package/dist/sandbox/core/shell/permissions.js +33 -0
  77. package/dist/sandbox/core/shell/workdir.d.ts +4 -0
  78. package/dist/sandbox/core/shell/workdir.js +19 -0
  79. package/dist/sandbox/core/stream-utils.d.ts +23 -0
  80. package/dist/sandbox/core/stream-utils.js +97 -0
  81. package/dist/sandbox/core/toast.d.ts +47 -0
  82. package/dist/sandbox/core/toast.js +73 -0
  83. package/dist/sandbox/core/types.d.ts +159 -0
  84. package/dist/sandbox/core/types.js +11 -0
  85. package/dist/sandbox/core/write/bom.d.ts +8 -0
  86. package/dist/sandbox/core/write/bom.js +15 -0
  87. package/dist/sandbox/core/write/config.d.ts +14 -0
  88. package/dist/sandbox/core/write/config.js +188 -0
  89. package/dist/sandbox/core/write/diagnostics.d.ts +19 -0
  90. package/dist/sandbox/core/write/diagnostics.js +120 -0
  91. package/dist/sandbox/core/write/diff.d.ts +7 -0
  92. package/dist/sandbox/core/write/diff.js +21 -0
  93. package/dist/sandbox/core/write/formatter.d.ts +16 -0
  94. package/dist/sandbox/core/write/formatter.js +51 -0
  95. package/dist/sandbox/core/write/index.d.ts +6 -0
  96. package/dist/sandbox/core/write/index.js +5 -0
  97. package/dist/sandbox/core/write/permissions.d.ts +13 -0
  98. package/dist/sandbox/core/write/permissions.js +19 -0
  99. package/dist/sandbox/core/write/pipeline.d.ts +48 -0
  100. package/dist/sandbox/core/write/pipeline.js +229 -0
  101. package/dist/sandbox/core/write/read-tracker.d.ts +13 -0
  102. package/dist/sandbox/core/write/read-tracker.js +30 -0
  103. package/dist/sandbox/git/host-git-manager.d.ts +40 -0
  104. package/dist/sandbox/git/host-git-manager.js +278 -0
  105. package/dist/sandbox/git/index.d.ts +5 -0
  106. package/dist/sandbox/git/index.js +5 -0
  107. package/dist/sandbox/git/sandbox-git-manager.d.ts +14 -0
  108. package/dist/sandbox/git/sandbox-git-manager.js +54 -0
  109. package/dist/sandbox/git/session-git-manager.d.ts +18 -0
  110. package/dist/sandbox/git/session-git-manager.js +85 -0
  111. package/dist/sandbox/index.d.ts +205 -0
  112. package/dist/sandbox/index.js +70 -0
  113. package/dist/sandbox/plugins/custom-tools.d.ts +203 -0
  114. package/dist/sandbox/plugins/custom-tools.js +15 -0
  115. package/dist/sandbox/plugins/session-events.d.ts +10 -0
  116. package/dist/sandbox/plugins/session-events.js +56 -0
  117. package/dist/sandbox/plugins/system-transform.d.ts +10 -0
  118. package/dist/sandbox/plugins/system-transform.js +23 -0
  119. package/dist/sandbox/tools/bash-output.d.ts +17 -0
  120. package/dist/sandbox/tools/bash-output.js +35 -0
  121. package/dist/sandbox/tools/bash-status.d.ts +13 -0
  122. package/dist/sandbox/tools/bash-status.js +29 -0
  123. package/dist/sandbox/tools/bash-stop.d.ts +13 -0
  124. package/dist/sandbox/tools/bash-stop.js +28 -0
  125. package/dist/sandbox/tools/bash.d.ts +26 -0
  126. package/dist/sandbox/tools/bash.js +120 -0
  127. package/dist/sandbox/tools/edit.d.ts +20 -0
  128. package/dist/sandbox/tools/edit.js +87 -0
  129. package/dist/sandbox/tools/get-preview-url.d.ts +17 -0
  130. package/dist/sandbox/tools/get-preview-url.js +16 -0
  131. package/dist/sandbox/tools/glob.d.ts +17 -0
  132. package/dist/sandbox/tools/glob.js +23 -0
  133. package/dist/sandbox/tools/grep.d.ts +17 -0
  134. package/dist/sandbox/tools/grep.js +23 -0
  135. package/dist/sandbox/tools/ls.d.ts +17 -0
  136. package/dist/sandbox/tools/ls.js +21 -0
  137. package/dist/sandbox/tools/lsp.d.ts +41 -0
  138. package/dist/sandbox/tools/lsp.js +198 -0
  139. package/dist/sandbox/tools/multiedit.d.ts +24 -0
  140. package/dist/sandbox/tools/multiedit.js +83 -0
  141. package/dist/sandbox/tools/patch.d.ts +14 -0
  142. package/dist/sandbox/tools/patch.js +260 -0
  143. package/dist/sandbox/tools/read.d.ts +22 -0
  144. package/dist/sandbox/tools/read.js +105 -0
  145. package/dist/sandbox/tools/write.d.ts +16 -0
  146. package/dist/sandbox/tools/write.js +27 -0
  147. package/dist/sandbox/tools.d.ts +200 -0
  148. package/dist/sandbox/tools.js +43 -0
  149. package/package.json +55 -0
@@ -0,0 +1,533 @@
1
+ import { spawn } from 'child_process';
2
+ import { readFile } from 'fs/promises';
3
+ import path from 'path';
4
+ import { fileURLToPath, pathToFileURL } from 'url';
5
+ import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node.js';
6
+ import { getLanguageId } from './language.js';
7
+ import { logger } from '../logger.js';
8
+ const DIAGNOSTICS_DEBOUNCE_MS = 150;
9
+ const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5000;
10
+ const DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS = 10000;
11
+ const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 3000;
12
+ const INITIALIZE_TIMEOUT_MS = 45000;
13
+ const SHUTDOWN_TIMEOUT_MS = 2000;
14
+ const FILE_CHANGE_CREATED = 1;
15
+ const FILE_CHANGE_CHANGED = 2;
16
+ const FILE_CHANGE_DELETED = 3;
17
+ const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2;
18
+ function withTimeout(promise, timeoutMs, label) {
19
+ return new Promise((resolve, reject) => {
20
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
21
+ promise.then((value) => {
22
+ clearTimeout(timer);
23
+ resolve(value);
24
+ }, (error) => {
25
+ clearTimeout(timer);
26
+ reject(error);
27
+ });
28
+ });
29
+ }
30
+ function getFilePath(uri) {
31
+ if (!uri.startsWith('file://'))
32
+ return undefined;
33
+ try {
34
+ return path.normalize(fileURLToPath(uri));
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ function getSyncKind(capabilities) {
41
+ const sync = capabilities?.textDocumentSync;
42
+ if (typeof sync === 'number')
43
+ return sync;
44
+ return sync?.change;
45
+ }
46
+ function endPosition(text) {
47
+ const lines = text.split(/\r\n|\r|\n/);
48
+ return { line: lines.length - 1, character: lines[lines.length - 1]?.length ?? 0 };
49
+ }
50
+ function dedupeDiagnostics(items) {
51
+ const seen = new Set();
52
+ return items.filter((item) => {
53
+ const key = JSON.stringify({
54
+ code: item.code,
55
+ severity: item.severity,
56
+ message: item.message,
57
+ source: item.source,
58
+ range: item.range,
59
+ });
60
+ if (seen.has(key))
61
+ return false;
62
+ seen.add(key);
63
+ return true;
64
+ });
65
+ }
66
+ function configurationValue(settings, section) {
67
+ if (!section)
68
+ return settings ?? null;
69
+ return (section.split('.').reduce((acc, key) => {
70
+ if (!acc || typeof acc !== 'object' || !(key in acc))
71
+ return undefined;
72
+ return acc[key];
73
+ }, settings) ?? null);
74
+ }
75
+ function shouldSeedDiagnosticsOnFirstPush(serverID) {
76
+ return serverID === 'typescript';
77
+ }
78
+ export class HostLspClient {
79
+ serverID;
80
+ root;
81
+ process;
82
+ connection;
83
+ initialization;
84
+ documents = new Map();
85
+ pushDiagnostics = new Map();
86
+ pullDiagnostics = new Map();
87
+ published = new Map();
88
+ diagnosticRegistrations = new Map();
89
+ registrationListeners = new Set();
90
+ diagnosticListeners = new Set();
91
+ capabilities;
92
+ closed = false;
93
+ constructor(input) {
94
+ this.serverID = input.serverID;
95
+ this.root = path.resolve(input.root);
96
+ this.process = input.process;
97
+ this.connection = input.connection;
98
+ this.initialization = input.initialization;
99
+ }
100
+ static async create(input) {
101
+ logger.info(`[lsp] starting ${input.serverID}: ${input.command.command} ${input.command.args.join(' ')}`);
102
+ const child = spawn(input.command.command, input.command.args, {
103
+ cwd: input.command.cwd,
104
+ env: input.command.env,
105
+ stdio: ['pipe', 'pipe', 'pipe'],
106
+ });
107
+ child.stderr.on('data', (data) => {
108
+ const text = data.toString().trim();
109
+ if (text)
110
+ logger.info(`[lsp:${input.serverID}:stderr] ${text.slice(0, 1000)}`);
111
+ });
112
+ const connection = createMessageConnection(new StreamMessageReader(child.stdout), new StreamMessageWriter(child.stdin));
113
+ const client = new HostLspClient({
114
+ serverID: input.serverID,
115
+ root: input.root,
116
+ process: child,
117
+ connection,
118
+ initialization: input.command.initialization,
119
+ });
120
+ client.registerHandlers();
121
+ connection.listen();
122
+ await client.initialize();
123
+ return client;
124
+ }
125
+ get diagnostics() {
126
+ const result = new Map();
127
+ for (const key of new Set([...this.pushDiagnostics.keys(), ...this.pullDiagnostics.keys()])) {
128
+ result.set(key, dedupeDiagnostics([...(this.pushDiagnostics.get(key) ?? []), ...(this.pullDiagnostics.get(key) ?? [])]));
129
+ }
130
+ return result;
131
+ }
132
+ async touchFile(filePath, diagnostics) {
133
+ const after = Date.now();
134
+ const version = await this.openOrChange(filePath);
135
+ if (diagnostics)
136
+ await this.waitForDiagnostics({ path: filePath, version, mode: diagnostics, after });
137
+ return version;
138
+ }
139
+ async closeFile(filePath) {
140
+ const normalizedPath = path.resolve(filePath);
141
+ const uri = pathToFileURL(normalizedPath).href;
142
+ const wasOpen = this.documents.delete(normalizedPath);
143
+ this.pushDiagnostics.delete(normalizedPath);
144
+ this.pullDiagnostics.delete(normalizedPath);
145
+ if (this.closed)
146
+ return;
147
+ await this.connection.sendNotification('workspace/didChangeWatchedFiles', {
148
+ changes: [{ uri, type: FILE_CHANGE_DELETED }],
149
+ });
150
+ if (wasOpen) {
151
+ await this.connection.sendNotification('textDocument/didClose', {
152
+ textDocument: { uri },
153
+ });
154
+ }
155
+ }
156
+ async hover(input) {
157
+ return this.connection.sendRequest('textDocument/hover', {
158
+ textDocument: { uri: pathToFileURL(input.filePath).href },
159
+ position: input.position,
160
+ });
161
+ }
162
+ async definition(input) {
163
+ return this.connection.sendRequest('textDocument/definition', {
164
+ textDocument: { uri: pathToFileURL(input.filePath).href },
165
+ position: input.position,
166
+ });
167
+ }
168
+ async references(input) {
169
+ return this.connection.sendRequest('textDocument/references', {
170
+ textDocument: { uri: pathToFileURL(input.filePath).href },
171
+ position: input.position,
172
+ context: { includeDeclaration: true },
173
+ });
174
+ }
175
+ async implementation(input) {
176
+ return this.connection.sendRequest('textDocument/implementation', {
177
+ textDocument: { uri: pathToFileURL(input.filePath).href },
178
+ position: input.position,
179
+ });
180
+ }
181
+ async prepareCallHierarchy(input) {
182
+ return this.connection.sendRequest('textDocument/prepareCallHierarchy', {
183
+ textDocument: { uri: pathToFileURL(input.filePath).href },
184
+ position: input.position,
185
+ });
186
+ }
187
+ async incomingCalls(item) {
188
+ return this.connection.sendRequest('callHierarchy/incomingCalls', { item });
189
+ }
190
+ async outgoingCalls(item) {
191
+ return this.connection.sendRequest('callHierarchy/outgoingCalls', { item });
192
+ }
193
+ async documentSymbol(filePath) {
194
+ return this.connection.sendRequest('textDocument/documentSymbol', {
195
+ textDocument: { uri: pathToFileURL(filePath).href },
196
+ });
197
+ }
198
+ async workspaceSymbol(query) {
199
+ return this.connection.sendRequest('workspace/symbol', { query });
200
+ }
201
+ async status() {
202
+ return {
203
+ id: this.serverID,
204
+ root: this.root,
205
+ status: this.closed ? 'closed' : 'connected',
206
+ openFiles: this.documents.size,
207
+ };
208
+ }
209
+ async shutdown() {
210
+ if (this.closed)
211
+ return;
212
+ this.closed = true;
213
+ logger.info(`[lsp] shutting down ${this.serverID} at ${this.root}`);
214
+ try {
215
+ await withTimeout(this.connection.sendRequest('shutdown', null), SHUTDOWN_TIMEOUT_MS, 'LSP shutdown');
216
+ }
217
+ catch (error) {
218
+ logger.warn(`[lsp] shutdown request failed for ${this.serverID}: ${error}`);
219
+ }
220
+ try {
221
+ await this.connection.sendNotification('exit');
222
+ }
223
+ catch (error) {
224
+ logger.warn(`[lsp] exit notification failed for ${this.serverID}: ${error}`);
225
+ }
226
+ try {
227
+ this.connection.end();
228
+ this.connection.dispose();
229
+ }
230
+ catch (error) {
231
+ logger.warn(`[lsp] connection dispose failed for ${this.serverID}: ${error}`);
232
+ }
233
+ if (!this.process.killed)
234
+ this.process.kill();
235
+ }
236
+ registerHandlers() {
237
+ this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
238
+ const payload = params;
239
+ if (!payload.uri || !Array.isArray(payload.diagnostics))
240
+ return;
241
+ const filePath = getFilePath(payload.uri);
242
+ if (!filePath)
243
+ return;
244
+ this.published.set(filePath, {
245
+ at: Date.now(),
246
+ version: typeof payload.version === 'number' ? payload.version : undefined,
247
+ });
248
+ if (shouldSeedDiagnosticsOnFirstPush(this.serverID) && !this.pushDiagnostics.has(filePath)) {
249
+ this.pushDiagnostics.set(filePath, payload.diagnostics);
250
+ }
251
+ else {
252
+ this.pushDiagnostics.set(filePath, payload.diagnostics);
253
+ }
254
+ for (const listener of [...this.diagnosticListeners])
255
+ listener(filePath);
256
+ });
257
+ this.connection.onRequest('window/workDoneProgress/create', async () => null);
258
+ this.connection.onRequest('workspace/configuration', async (params) => {
259
+ const items = params.items ?? [];
260
+ return items.map((item) => configurationValue(this.initialization, item.section));
261
+ });
262
+ this.connection.onRequest('client/registerCapability', async (params) => {
263
+ const registrations = params.registrations ?? [];
264
+ let changed = false;
265
+ for (const registration of registrations) {
266
+ if (registration.method !== 'textDocument/diagnostic')
267
+ continue;
268
+ this.diagnosticRegistrations.set(registration.id, registration);
269
+ changed = true;
270
+ }
271
+ if (changed)
272
+ this.emitRegistrationChange();
273
+ return null;
274
+ });
275
+ this.connection.onRequest('client/unregisterCapability', async (params) => {
276
+ const registrations = params.unregisterations ?? [];
277
+ let changed = false;
278
+ for (const registration of registrations) {
279
+ if (registration.method !== 'textDocument/diagnostic')
280
+ continue;
281
+ this.diagnosticRegistrations.delete(registration.id);
282
+ changed = true;
283
+ }
284
+ if (changed)
285
+ this.emitRegistrationChange();
286
+ return null;
287
+ });
288
+ this.connection.onRequest('workspace/workspaceFolders', async () => [
289
+ { name: path.basename(this.root) || 'workspace', uri: pathToFileURL(this.root).href },
290
+ ]);
291
+ this.connection.onRequest('workspace/diagnostic/refresh', async () => null);
292
+ }
293
+ async initialize() {
294
+ const initialized = await withTimeout(this.connection.sendRequest('initialize', {
295
+ rootUri: pathToFileURL(this.root).href,
296
+ processId: this.process.pid,
297
+ workspaceFolders: [{ name: path.basename(this.root) || 'workspace', uri: pathToFileURL(this.root).href }],
298
+ initializationOptions: { ...(this.initialization ?? {}) },
299
+ capabilities: {
300
+ window: { workDoneProgress: true },
301
+ workspace: {
302
+ configuration: true,
303
+ didChangeWatchedFiles: { dynamicRegistration: true },
304
+ diagnostics: { refreshSupport: false },
305
+ workspaceFolders: true,
306
+ },
307
+ textDocument: {
308
+ synchronization: { didOpen: true, didChange: true, didClose: true },
309
+ diagnostic: { dynamicRegistration: true, relatedDocumentSupport: true },
310
+ publishDiagnostics: { versionSupport: false },
311
+ hover: { contentFormat: ['markdown', 'plaintext'] },
312
+ definition: { linkSupport: true },
313
+ implementation: { linkSupport: true },
314
+ references: {},
315
+ callHierarchy: { dynamicRegistration: false },
316
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
317
+ },
318
+ },
319
+ }), INITIALIZE_TIMEOUT_MS, `LSP initialize ${this.serverID}`);
320
+ this.capabilities = initialized.capabilities;
321
+ await this.connection.sendNotification('initialized', {});
322
+ if (this.initialization) {
323
+ await this.connection.sendNotification('workspace/didChangeConfiguration', { settings: this.initialization });
324
+ }
325
+ logger.info(`[lsp] initialized ${this.serverID} at ${this.root}`);
326
+ }
327
+ async openOrChange(filePath) {
328
+ const normalizedPath = path.resolve(filePath);
329
+ const uri = pathToFileURL(normalizedPath).href;
330
+ const text = await readFile(normalizedPath, 'utf8');
331
+ const existing = this.documents.get(normalizedPath);
332
+ if (existing) {
333
+ await this.connection.sendNotification('workspace/didChangeWatchedFiles', {
334
+ changes: [{ uri, type: FILE_CHANGE_CHANGED }],
335
+ });
336
+ const version = existing.version + 1;
337
+ this.documents.set(normalizedPath, { version, text });
338
+ await this.connection.sendNotification('textDocument/didChange', {
339
+ textDocument: { uri, version },
340
+ contentChanges: getSyncKind(this.capabilities) === TEXT_DOCUMENT_SYNC_INCREMENTAL
341
+ ? [{ range: { start: { line: 0, character: 0 }, end: endPosition(existing.text) }, text }]
342
+ : [{ text }],
343
+ });
344
+ return version;
345
+ }
346
+ this.pushDiagnostics.delete(normalizedPath);
347
+ this.pullDiagnostics.delete(normalizedPath);
348
+ await this.connection.sendNotification('workspace/didChangeWatchedFiles', {
349
+ changes: [{ uri, type: FILE_CHANGE_CREATED }],
350
+ });
351
+ await this.connection.sendNotification('textDocument/didOpen', {
352
+ textDocument: {
353
+ uri,
354
+ languageId: getLanguageId(normalizedPath),
355
+ version: 0,
356
+ text,
357
+ },
358
+ });
359
+ this.documents.set(normalizedPath, { version: 0, text });
360
+ return 0;
361
+ }
362
+ async waitForDiagnostics(request) {
363
+ const normalizedPath = path.resolve(request.path);
364
+ const startedAt = request.after ?? Date.now();
365
+ const timeout = request.mode === 'document' ? DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS : DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS;
366
+ const pull = request.mode === 'document' ? await this.requestDocumentDiagnostics(normalizedPath) : await this.requestFullDiagnostics(normalizedPath);
367
+ if (pull.matched || pull.handled)
368
+ return;
369
+ await Promise.race([
370
+ this.waitForFreshPush({ path: normalizedPath, version: request.version, after: startedAt, timeout }),
371
+ this.waitForRegistrationChange(timeout),
372
+ ]);
373
+ if (request.mode === 'document')
374
+ await this.requestDocumentDiagnostics(normalizedPath);
375
+ else
376
+ await this.requestFullDiagnostics(normalizedPath);
377
+ }
378
+ async requestDiagnosticReport(filePath, identifier) {
379
+ const report = await withTimeout(this.connection.sendRequest('textDocument/diagnostic', {
380
+ ...(identifier ? { identifier } : {}),
381
+ textDocument: { uri: pathToFileURL(filePath).href },
382
+ }), DIAGNOSTICS_REQUEST_TIMEOUT_MS, 'textDocument/diagnostic').catch(() => null);
383
+ if (!report)
384
+ return { handled: false, matched: false, byFile: new Map() };
385
+ const byFile = new Map();
386
+ let handled = false;
387
+ let matched = false;
388
+ const push = (target, items) => {
389
+ byFile.set(target, [...(byFile.get(target) ?? []), ...items]);
390
+ };
391
+ if (Array.isArray(report.items)) {
392
+ push(filePath, report.items);
393
+ handled = true;
394
+ matched = true;
395
+ }
396
+ const relatedDocuments = (report.relatedDocuments ?? {});
397
+ for (const [uri, related] of Object.entries(relatedDocuments)) {
398
+ const relatedPath = getFilePath(uri);
399
+ if (!relatedPath || !Array.isArray(related.items))
400
+ continue;
401
+ push(relatedPath, related.items);
402
+ handled = true;
403
+ matched = matched || relatedPath === filePath;
404
+ }
405
+ return { handled, matched, byFile };
406
+ }
407
+ async requestWorkspaceDiagnosticReport(identifier) {
408
+ const report = await withTimeout(this.connection.sendRequest('workspace/diagnostic', {
409
+ ...(identifier ? { identifier } : {}),
410
+ previousResultIds: [],
411
+ }), DIAGNOSTICS_REQUEST_TIMEOUT_MS, 'workspace/diagnostic').catch(() => null);
412
+ if (!report)
413
+ return { handled: false, matched: false, byFile: new Map() };
414
+ const byFile = new Map();
415
+ for (const item of report.items ?? []) {
416
+ const filePath = item.uri ? getFilePath(item.uri) : undefined;
417
+ if (!filePath || !Array.isArray(item.items))
418
+ continue;
419
+ byFile.set(filePath, [...(byFile.get(filePath) ?? []), ...item.items]);
420
+ }
421
+ return { handled: true, matched: false, byFile };
422
+ }
423
+ async requestDocumentDiagnostics(filePath) {
424
+ const identifiers = this.documentDiagnosticIdentifiers();
425
+ const supported = Boolean(this.capabilities?.diagnosticProvider) || identifiers.length > 0;
426
+ if (!supported)
427
+ return { handled: false, matched: false };
428
+ return this.mergeDiagnosticResults(filePath, await Promise.all([this.requestDiagnosticReport(filePath), ...identifiers.map((id) => this.requestDiagnosticReport(filePath, id))]));
429
+ }
430
+ async requestFullDiagnostics(filePath) {
431
+ const documentIdentifiers = this.documentDiagnosticIdentifiers();
432
+ const workspaceIdentifiers = this.workspaceDiagnosticIdentifiers();
433
+ const documentSupported = Boolean(this.capabilities?.diagnosticProvider) || documentIdentifiers.length > 0;
434
+ const workspaceSupported = workspaceIdentifiers.length > 0;
435
+ if (!documentSupported && !workspaceSupported)
436
+ return { handled: false, matched: false };
437
+ return this.mergeDiagnosticResults(filePath, await Promise.all([
438
+ ...(documentSupported ? [this.requestDiagnosticReport(filePath)] : []),
439
+ ...documentIdentifiers.map((id) => this.requestDiagnosticReport(filePath, id)),
440
+ ...(workspaceSupported ? [this.requestWorkspaceDiagnosticReport()] : []),
441
+ ...workspaceIdentifiers.map((id) => this.requestWorkspaceDiagnosticReport(id)),
442
+ ]));
443
+ }
444
+ mergeDiagnosticResults(filePath, results) {
445
+ const handled = results.some((result) => result.handled);
446
+ const matched = results.some((result) => result.matched || result.byFile.has(filePath));
447
+ if (!handled)
448
+ return { handled: false, matched: false };
449
+ const merged = new Map();
450
+ for (const result of results) {
451
+ for (const [target, items] of result.byFile.entries()) {
452
+ merged.set(target, [...(merged.get(target) ?? []), ...items]);
453
+ }
454
+ }
455
+ if (matched && !merged.has(filePath))
456
+ merged.set(filePath, []);
457
+ for (const [target, items] of merged.entries()) {
458
+ this.pullDiagnostics.set(target, dedupeDiagnostics(items));
459
+ }
460
+ return { handled, matched };
461
+ }
462
+ documentDiagnosticIdentifiers() {
463
+ return [
464
+ ...new Set([...this.diagnosticRegistrations.values()]
465
+ .filter((registration) => registration.registerOptions?.workspaceDiagnostics !== true)
466
+ .flatMap((registration) => registration.registerOptions?.identifier ?? [])),
467
+ ];
468
+ }
469
+ workspaceDiagnosticIdentifiers() {
470
+ return [
471
+ ...new Set([...this.diagnosticRegistrations.values()]
472
+ .filter((registration) => registration.registerOptions?.workspaceDiagnostics === true)
473
+ .flatMap((registration) => registration.registerOptions?.identifier ?? [])),
474
+ ];
475
+ }
476
+ waitForFreshPush(request) {
477
+ return new Promise((resolve) => {
478
+ let finished = false;
479
+ let debounceTimer;
480
+ const timers = {};
481
+ const finish = (value) => {
482
+ if (finished)
483
+ return;
484
+ finished = true;
485
+ if (debounceTimer)
486
+ clearTimeout(debounceTimer);
487
+ if (timers.timeout)
488
+ clearTimeout(timers.timeout);
489
+ this.diagnosticListeners.delete(listener);
490
+ resolve(value);
491
+ };
492
+ const schedule = () => {
493
+ const hit = this.published.get(request.path);
494
+ if (!hit)
495
+ return;
496
+ if (typeof hit.version === 'number' && hit.version !== request.version)
497
+ return;
498
+ if (hit.at < request.after && hit.version !== request.version)
499
+ return;
500
+ if (debounceTimer)
501
+ clearTimeout(debounceTimer);
502
+ debounceTimer = setTimeout(() => finish(true), Math.max(0, DIAGNOSTICS_DEBOUNCE_MS - (Date.now() - hit.at)));
503
+ };
504
+ const listener = (filePath) => {
505
+ if (filePath === request.path)
506
+ schedule();
507
+ };
508
+ this.diagnosticListeners.add(listener);
509
+ timers.timeout = setTimeout(() => finish(false), request.timeout);
510
+ schedule();
511
+ });
512
+ }
513
+ waitForRegistrationChange(timeout) {
514
+ return new Promise((resolve) => {
515
+ let finished = false;
516
+ const finish = (value) => {
517
+ if (finished)
518
+ return;
519
+ finished = true;
520
+ clearTimeout(timer);
521
+ this.registrationListeners.delete(listener);
522
+ resolve(value);
523
+ };
524
+ const listener = () => finish(true);
525
+ const timer = setTimeout(() => finish(false), timeout);
526
+ this.registrationListeners.add(listener);
527
+ });
528
+ }
529
+ emitRegistrationChange() {
530
+ for (const listener of [...this.registrationListeners])
531
+ listener();
532
+ }
533
+ }
@@ -0,0 +1,13 @@
1
+ export type HostLspServerConfig = {
2
+ disabled?: boolean;
3
+ command?: string[];
4
+ env?: Record<string, string>;
5
+ initialization?: Record<string, unknown>;
6
+ extensions?: string[];
7
+ rootMarkers?: string[];
8
+ };
9
+ export type HostLspConfig = {
10
+ enabled: boolean;
11
+ servers: Record<string, HostLspServerConfig>;
12
+ };
13
+ export declare function loadHostLspConfig(worktree: string): HostLspConfig;
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import path from 'path';
3
+ export function loadHostLspConfig(worktree) {
4
+ const configPath = path.join(worktree, '.opencode', 'sandbox.json');
5
+ if (!existsSync(configPath))
6
+ return { enabled: true, servers: {} };
7
+ try {
8
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
9
+ if (raw.lsp === false)
10
+ return { enabled: false, servers: {} };
11
+ if (raw.lsp === true || raw.lsp == null)
12
+ return { enabled: true, servers: {} };
13
+ if (typeof raw.lsp !== 'object')
14
+ return { enabled: true, servers: {} };
15
+ const servers = {};
16
+ for (const [serverID, value] of Object.entries(raw.lsp)) {
17
+ if (!value || typeof value !== 'object')
18
+ continue;
19
+ const item = value;
20
+ servers[serverID] = {
21
+ disabled: Boolean(item.disabled),
22
+ command: Array.isArray(item.command) ? item.command.map(String) : undefined,
23
+ env: item.env && typeof item.env === 'object' ? Object.fromEntries(Object.entries(item.env).map(([k, v]) => [k, String(v)])) : undefined,
24
+ initialization: item.initialization && typeof item.initialization === 'object'
25
+ ? item.initialization
26
+ : undefined,
27
+ extensions: Array.isArray(item.extensions) ? item.extensions.map(String) : undefined,
28
+ rootMarkers: Array.isArray(item.rootMarkers) ? item.rootMarkers.map(String) : undefined,
29
+ };
30
+ }
31
+ return { enabled: true, servers };
32
+ }
33
+ catch {
34
+ return { enabled: true, servers: {} };
35
+ }
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { Diagnostic } from 'vscode-languageserver-types';
2
+ export type DiagnosticCounts = {
3
+ errors: number;
4
+ warnings: number;
5
+ information: number;
6
+ hints: number;
7
+ };
8
+ export declare function countDiagnostics(diagnostics: Diagnostic[]): DiagnosticCounts;
9
+ export declare function hasActionableDiagnostics(diagnostics: Diagnostic[]): boolean;
10
+ export declare function severityLabel(severity?: Diagnostic['severity']): "INFO" | "ERROR" | "WARN" | "HINT";
11
+ export declare function formatLspDiagnosticOutput(filePath: string, diagnostics: Diagnostic[]): string;
12
+ export declare function formatLspUnavailable(error: unknown): string;
@@ -0,0 +1,65 @@
1
+ import { DiagnosticSeverity } from 'vscode-languageserver-types';
2
+ import { HostLspUnavailableError } from './manager.js';
3
+ export function countDiagnostics(diagnostics) {
4
+ return diagnostics.reduce((counts, diagnostic) => {
5
+ switch (diagnostic.severity) {
6
+ case DiagnosticSeverity.Error:
7
+ counts.errors++;
8
+ break;
9
+ case DiagnosticSeverity.Warning:
10
+ counts.warnings++;
11
+ break;
12
+ case DiagnosticSeverity.Information:
13
+ counts.information++;
14
+ break;
15
+ case DiagnosticSeverity.Hint:
16
+ counts.hints++;
17
+ break;
18
+ default:
19
+ counts.warnings++;
20
+ }
21
+ return counts;
22
+ }, { errors: 0, warnings: 0, information: 0, hints: 0 });
23
+ }
24
+ export function hasActionableDiagnostics(diagnostics) {
25
+ const counts = countDiagnostics(diagnostics);
26
+ return counts.errors > 0 || counts.warnings > 0;
27
+ }
28
+ export function severityLabel(severity) {
29
+ switch (severity) {
30
+ case DiagnosticSeverity.Error:
31
+ return 'ERROR';
32
+ case DiagnosticSeverity.Warning:
33
+ return 'WARN';
34
+ case DiagnosticSeverity.Information:
35
+ return 'INFO';
36
+ case DiagnosticSeverity.Hint:
37
+ return 'HINT';
38
+ default:
39
+ return 'WARN';
40
+ }
41
+ }
42
+ export function formatLspDiagnosticOutput(filePath, diagnostics) {
43
+ if (diagnostics.length === 0)
44
+ return '';
45
+ const counts = countDiagnostics(diagnostics);
46
+ const title = counts.errors > 0
47
+ ? `LSP errors detected in ${filePath}, please fix:`
48
+ : counts.warnings > 0
49
+ ? `LSP warnings in ${filePath}:`
50
+ : `LSP diagnostics in ${filePath}:`;
51
+ const lines = diagnostics.map((diagnostic) => {
52
+ const line = diagnostic.range.start.line + 1;
53
+ const character = diagnostic.range.start.character + 1;
54
+ const source = diagnostic.source ? ` [${diagnostic.source}]` : '';
55
+ const code = diagnostic.code != null ? ` (${String(diagnostic.code)})` : '';
56
+ return ` ${severityLabel(diagnostic.severity)} ${filePath}:${line}:${character}${source}${code}: ${diagnostic.message}`;
57
+ });
58
+ return `${title}\n${lines.join('\n')}`;
59
+ }
60
+ export function formatLspUnavailable(error) {
61
+ if (error instanceof HostLspUnavailableError)
62
+ return `Host LSP unavailable: ${error.message}`;
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ return `Host LSP failed: ${message}`;
65
+ }
@@ -0,0 +1,4 @@
1
+ export { hostLspManager, HostLspManager, HostLspUnavailableError } from './manager.js';
2
+ export { formatLspDiagnosticOutput, formatLspUnavailable, hasActionableDiagnostics } from './diagnostics.js';
3
+ export { notifyHostLspAfterWrite, notifyHostLspAfterDelete, warmHostLspFile, waitForHostFile } from './tooling.js';
4
+ export { getLanguageId, LANGUAGE_EXTENSIONS } from './language.js';
@@ -0,0 +1,4 @@
1
+ export { hostLspManager, HostLspManager, HostLspUnavailableError } from './manager.js';
2
+ export { formatLspDiagnosticOutput, formatLspUnavailable, hasActionableDiagnostics } from './diagnostics.js';
3
+ export { notifyHostLspAfterWrite, notifyHostLspAfterDelete, warmHostLspFile, waitForHostFile } from './tooling.js';
4
+ export { getLanguageId, LANGUAGE_EXTENSIONS } from './language.js';