@plaited/development-skills 0.3.5

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 (40) hide show
  1. package/.claude/commands/lsp-analyze.md +66 -0
  2. package/.claude/commands/lsp-find.md +51 -0
  3. package/.claude/commands/lsp-hover.md +48 -0
  4. package/.claude/commands/lsp-refs.md +55 -0
  5. package/.claude/commands/scaffold-rules.md +221 -0
  6. package/.claude/commands/validate-skill.md +29 -0
  7. package/.claude/rules/accuracy.md +64 -0
  8. package/.claude/rules/bun-apis.md +80 -0
  9. package/.claude/rules/code-review.md +276 -0
  10. package/.claude/rules/git-workflow.md +66 -0
  11. package/.claude/rules/github.md +154 -0
  12. package/.claude/rules/testing.md +125 -0
  13. package/.claude/settings.local.json +47 -0
  14. package/.claude/skills/code-documentation/SKILL.md +47 -0
  15. package/.claude/skills/code-documentation/references/internal-templates.md +113 -0
  16. package/.claude/skills/code-documentation/references/maintenance.md +164 -0
  17. package/.claude/skills/code-documentation/references/public-api-templates.md +100 -0
  18. package/.claude/skills/code-documentation/references/type-documentation.md +116 -0
  19. package/.claude/skills/code-documentation/references/workflow.md +60 -0
  20. package/.claude/skills/scaffold-rules/SKILL.md +97 -0
  21. package/.claude/skills/typescript-lsp/SKILL.md +239 -0
  22. package/.claude/skills/validate-skill/SKILL.md +105 -0
  23. package/LICENSE +15 -0
  24. package/README.md +149 -0
  25. package/bin/cli.ts +109 -0
  26. package/package.json +57 -0
  27. package/src/lsp-analyze.ts +223 -0
  28. package/src/lsp-client.ts +400 -0
  29. package/src/lsp-find.ts +100 -0
  30. package/src/lsp-hover.ts +87 -0
  31. package/src/lsp-references.ts +83 -0
  32. package/src/lsp-symbols.ts +73 -0
  33. package/src/resolve-file-path.ts +28 -0
  34. package/src/scaffold-rules.ts +435 -0
  35. package/src/tests/fixtures/sample.ts +27 -0
  36. package/src/tests/lsp-client.spec.ts +180 -0
  37. package/src/tests/resolve-file-path.spec.ts +33 -0
  38. package/src/tests/scaffold-rules.spec.ts +286 -0
  39. package/src/tests/validate-skill.spec.ts +231 -0
  40. package/src/validate-skill.ts +492 -0
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Batch analysis script for TypeScript/JavaScript files
4
+ *
5
+ * Performs multiple LSP queries in a single session for efficiency.
6
+ * Useful for understanding a file before making changes.
7
+ *
8
+ * Usage: bun lsp-analyze.ts <file> [options]
9
+ *
10
+ * Options:
11
+ * --symbols, -s List all symbols in the file
12
+ * --exports, -e List only exported symbols
13
+ * --hover <line:char> Get type info at position (can be repeated)
14
+ * --refs <line:char> Find references at position (can be repeated)
15
+ * --all Run all analyses (symbols + exports)
16
+ */
17
+
18
+ import { parseArgs } from 'node:util'
19
+ import { LspClient } from './lsp-client.ts'
20
+ import { resolveFilePath } from './resolve-file-path.ts'
21
+
22
+ type SymbolInfo = {
23
+ name: string
24
+ kind: number
25
+ range: { start: { line: number; character: number }; end: { line: number; character: number } }
26
+ children?: SymbolInfo[]
27
+ }
28
+
29
+ type AnalysisResult = {
30
+ file: string
31
+ symbols?: Array<{ name: string; kind: string; line: number }>
32
+ exports?: Array<{ name: string; kind: string; line: number }>
33
+ hovers?: Array<{ position: string; content: unknown }>
34
+ references?: Array<{ position: string; locations: unknown }>
35
+ }
36
+
37
+ const symbolKindNames: Record<number, string> = {
38
+ 1: 'File',
39
+ 2: 'Module',
40
+ 3: 'Namespace',
41
+ 4: 'Package',
42
+ 5: 'Class',
43
+ 6: 'Method',
44
+ 7: 'Property',
45
+ 8: 'Field',
46
+ 9: 'Constructor',
47
+ 10: 'Enum',
48
+ 11: 'Interface',
49
+ 12: 'Function',
50
+ 13: 'Variable',
51
+ 14: 'Constant',
52
+ 15: 'String',
53
+ 16: 'Number',
54
+ 17: 'Boolean',
55
+ 18: 'Array',
56
+ 19: 'Object',
57
+ 20: 'Key',
58
+ 21: 'Null',
59
+ 22: 'EnumMember',
60
+ 23: 'Struct',
61
+ 24: 'Event',
62
+ 25: 'Operator',
63
+ 26: 'TypeParameter',
64
+ }
65
+
66
+ const extractSymbols = (symbols: SymbolInfo[], prefix = ''): Array<{ name: string; kind: string; line: number }> => {
67
+ const result: Array<{ name: string; kind: string; line: number }> = []
68
+ for (const sym of symbols) {
69
+ result.push({
70
+ name: prefix ? `${prefix}.${sym.name}` : sym.name,
71
+ kind: symbolKindNames[sym.kind] || `Unknown(${sym.kind})`,
72
+ line: sym.range.start.line,
73
+ })
74
+ if (sym.children) {
75
+ result.push(...extractSymbols(sym.children, sym.name))
76
+ }
77
+ }
78
+ return result
79
+ }
80
+
81
+ /**
82
+ * Batch analysis for TypeScript/JavaScript files
83
+ *
84
+ * @param args - Command line arguments
85
+ */
86
+ export const lspAnalyze = async (args: string[]) => {
87
+ const { values, positionals } = parseArgs({
88
+ args,
89
+ options: {
90
+ symbols: { type: 'boolean', short: 's' },
91
+ exports: { type: 'boolean', short: 'e' },
92
+ hover: { type: 'string', multiple: true },
93
+ refs: { type: 'string', multiple: true },
94
+ all: { type: 'boolean' },
95
+ help: { type: 'boolean', short: 'h' },
96
+ },
97
+ allowPositionals: true,
98
+ })
99
+
100
+ if (values.help || positionals.length === 0) {
101
+ console.log(`
102
+ LSP Analyze - Batch analysis for TypeScript/JavaScript files
103
+
104
+ Usage: lsp-analyze <file> [options]
105
+
106
+ Options:
107
+ --symbols, -s List all symbols in the file
108
+ --exports, -e List only exported symbols
109
+ --hover <line:char> Get type info at position (can be repeated)
110
+ --refs <line:char> Find references at position (can be repeated)
111
+ --all Run all analyses (symbols + exports)
112
+ --help, -h Show this help
113
+
114
+ Examples:
115
+ lsp-analyze src/app.ts --all
116
+ lsp-analyze src/app.ts --symbols
117
+ lsp-analyze src/app.ts --hover 50:15 --hover 60:20
118
+ lsp-analyze src/app.ts --refs 10:8
119
+ `)
120
+ process.exit(0)
121
+ }
122
+
123
+ const filePath = positionals[0]
124
+ if (!filePath) {
125
+ console.error('Error: File path required')
126
+ process.exit(1)
127
+ }
128
+ const absolutePath = await resolveFilePath(filePath)
129
+ const uri = `file://${absolutePath}`
130
+ const rootUri = `file://${process.cwd()}`
131
+
132
+ const client = new LspClient({ rootUri })
133
+
134
+ try {
135
+ await client.start()
136
+
137
+ const file = Bun.file(absolutePath)
138
+ if (!(await file.exists())) {
139
+ console.error(`Error: File not found: ${absolutePath}`)
140
+ process.exit(1)
141
+ }
142
+
143
+ const text = await file.text()
144
+ const languageId = absolutePath.endsWith('.tsx')
145
+ ? 'typescriptreact'
146
+ : absolutePath.endsWith('.ts')
147
+ ? 'typescript'
148
+ : absolutePath.endsWith('.jsx')
149
+ ? 'javascriptreact'
150
+ : 'javascript'
151
+
152
+ client.openDocument(uri, languageId, 1, text)
153
+
154
+ const result: AnalysisResult = { file: filePath }
155
+
156
+ // Get symbols if requested
157
+ if (values.symbols || values.exports || values.all) {
158
+ const symbols = (await client.documentSymbols(uri)) as SymbolInfo[]
159
+ const extracted = extractSymbols(symbols)
160
+
161
+ if (values.symbols || values.all) {
162
+ result.symbols = extracted
163
+ }
164
+
165
+ if (values.exports || values.all) {
166
+ // Filter to only top-level exports (items that start with export in source)
167
+ const lines = text.split('\n')
168
+ result.exports = extracted.filter((sym) => {
169
+ const line = lines[sym.line]
170
+ return line?.includes('export')
171
+ })
172
+ }
173
+ }
174
+
175
+ // Get hover info if requested
176
+ if (values.hover?.length) {
177
+ result.hovers = []
178
+ for (const pos of values.hover) {
179
+ const parts = pos.split(':')
180
+ const lineStr = parts[0] ?? ''
181
+ const charStr = parts[1] ?? ''
182
+ const line = parseInt(lineStr, 10)
183
+ const char = parseInt(charStr, 10)
184
+
185
+ if (!Number.isNaN(line) && !Number.isNaN(char)) {
186
+ const hover = await client.hover(uri, line, char)
187
+ result.hovers.push({ position: pos, content: hover })
188
+ }
189
+ }
190
+ }
191
+
192
+ // Get references if requested
193
+ if (values.refs?.length) {
194
+ result.references = []
195
+ for (const pos of values.refs) {
196
+ const parts = pos.split(':')
197
+ const lineStr = parts[0] ?? ''
198
+ const charStr = parts[1] ?? ''
199
+ const line = parseInt(lineStr, 10)
200
+ const char = parseInt(charStr, 10)
201
+
202
+ if (!Number.isNaN(line) && !Number.isNaN(char)) {
203
+ const refs = await client.references(uri, line, char, true)
204
+ result.references.push({ position: pos, locations: refs })
205
+ }
206
+ }
207
+ }
208
+
209
+ client.closeDocument(uri)
210
+ await client.stop()
211
+
212
+ console.log(JSON.stringify(result, null, 2))
213
+ } catch (error) {
214
+ console.error(`Error: ${error}`)
215
+ await client.stop()
216
+ process.exit(1)
217
+ }
218
+ }
219
+
220
+ // Keep executable entry point for direct execution
221
+ if (import.meta.main) {
222
+ await lspAnalyze(Bun.argv.slice(2))
223
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * TypeScript Language Server client using Bun.spawn
3
+ *
4
+ * @remarks
5
+ * Spawns typescript-language-server as a subprocess and communicates via LSP JSON-RPC protocol.
6
+ * Uses Bun's native spawn API for process management.
7
+ *
8
+ * @internal
9
+ */
10
+
11
+ import type { Subprocess } from 'bun'
12
+
13
+ type JsonRpcRequest = {
14
+ jsonrpc: '2.0'
15
+ id: number
16
+ method: string
17
+ params?: unknown
18
+ }
19
+
20
+ type JsonRpcResponse = {
21
+ jsonrpc: '2.0'
22
+ id: number
23
+ result?: unknown
24
+ error?: { code: number; message: string; data?: unknown }
25
+ }
26
+
27
+ type JsonRpcNotification = {
28
+ jsonrpc: '2.0'
29
+ method: string
30
+ params?: unknown
31
+ }
32
+
33
+ type PendingRequest = {
34
+ resolve: (value: unknown) => void
35
+ reject: (error: Error) => void
36
+ timer?: ReturnType<typeof setTimeout>
37
+ }
38
+
39
+ /**
40
+ * LSP Client that manages a typescript-language-server subprocess
41
+ */
42
+ export class LspClient {
43
+ #process: Subprocess | null = null
44
+ #requestId = 0
45
+ #pendingRequests = new Map<number, PendingRequest>()
46
+ #buffer = new Uint8Array(0)
47
+ #contentLength = -1
48
+ #initialized = false
49
+ #rootUri: string
50
+ #serverCommand: string[]
51
+ #requestTimeout: number
52
+
53
+ constructor({
54
+ rootUri,
55
+ command = ['bun', 'typescript-language-server', '--stdio'],
56
+ requestTimeout = 30000,
57
+ }: {
58
+ rootUri: string
59
+ command?: string[]
60
+ requestTimeout?: number
61
+ }) {
62
+ this.#rootUri = rootUri
63
+ this.#serverCommand = command
64
+ this.#requestTimeout = requestTimeout
65
+ }
66
+
67
+ /**
68
+ * Start the LSP server subprocess
69
+ */
70
+ async start(): Promise<void> {
71
+ if (this.#process) {
72
+ throw new Error('LSP server already running')
73
+ }
74
+
75
+ this.#process = Bun.spawn(this.#serverCommand, {
76
+ stdin: 'pipe',
77
+ stdout: 'pipe',
78
+ stderr: 'pipe',
79
+ })
80
+
81
+ // Start reading stdout
82
+ this.#readOutput()
83
+
84
+ // Initialize the LSP connection
85
+ await this.#initialize()
86
+ }
87
+
88
+ /**
89
+ * Stop the LSP server subprocess
90
+ */
91
+ async stop(): Promise<void> {
92
+ if (!this.#process) return
93
+
94
+ // Send shutdown request
95
+ try {
96
+ await this.request('shutdown', null)
97
+ this.notify('exit')
98
+ } catch {
99
+ // Ignore errors during shutdown
100
+ }
101
+
102
+ this.#process.kill()
103
+ this.#process = null
104
+ this.#initialized = false
105
+ }
106
+
107
+ /**
108
+ * Check if the LSP server is running
109
+ */
110
+ isRunning(): boolean {
111
+ return this.#process !== null && this.#initialized
112
+ }
113
+
114
+ /**
115
+ * Send a request to the LSP server and wait for response
116
+ */
117
+ async request<T = unknown>(method: string, params: unknown): Promise<T> {
118
+ if (!this.#process) {
119
+ throw new Error('LSP server not running')
120
+ }
121
+
122
+ this.#requestId += 1
123
+ const id = this.#requestId
124
+ const request: JsonRpcRequest = {
125
+ jsonrpc: '2.0',
126
+ id,
127
+ method,
128
+ params: params ?? undefined,
129
+ }
130
+
131
+ return new Promise((resolve, reject) => {
132
+ const timer = setTimeout(() => {
133
+ this.#pendingRequests.delete(id)
134
+ reject(new Error(`LSP request timeout: ${method} (id=${id})`))
135
+ }, this.#requestTimeout)
136
+
137
+ this.#pendingRequests.set(id, {
138
+ resolve: resolve as (value: unknown) => void,
139
+ reject,
140
+ timer,
141
+ })
142
+ this.#send(request)
143
+ })
144
+ }
145
+
146
+ /**
147
+ * Send a notification to the LSP server (no response expected)
148
+ */
149
+ notify(method: string, params?: unknown): void {
150
+ if (!this.#process) {
151
+ throw new Error('LSP server not running')
152
+ }
153
+
154
+ const notification: JsonRpcNotification = {
155
+ jsonrpc: '2.0',
156
+ method,
157
+ params,
158
+ }
159
+
160
+ this.#send(notification)
161
+ }
162
+
163
+ // LSP Methods
164
+
165
+ /**
166
+ * textDocument/hover - Get hover information at a position
167
+ */
168
+ async hover(uri: string, line: number, character: number): Promise<unknown> {
169
+ return this.request('textDocument/hover', {
170
+ textDocument: { uri },
171
+ position: { line, character },
172
+ })
173
+ }
174
+
175
+ /**
176
+ * textDocument/definition - Go to definition
177
+ */
178
+ async definition(uri: string, line: number, character: number): Promise<unknown> {
179
+ return this.request('textDocument/definition', {
180
+ textDocument: { uri },
181
+ position: { line, character },
182
+ })
183
+ }
184
+
185
+ /**
186
+ * textDocument/references - Find all references
187
+ */
188
+ async references(uri: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
189
+ return this.request('textDocument/references', {
190
+ textDocument: { uri },
191
+ position: { line, character },
192
+ context: { includeDeclaration },
193
+ })
194
+ }
195
+
196
+ /**
197
+ * textDocument/completion - Get completions at a position
198
+ */
199
+ async completion(uri: string, line: number, character: number): Promise<unknown> {
200
+ return this.request('textDocument/completion', {
201
+ textDocument: { uri },
202
+ position: { line, character },
203
+ })
204
+ }
205
+
206
+ /**
207
+ * textDocument/signatureHelp - Get signature help
208
+ */
209
+ async signatureHelp(uri: string, line: number, character: number): Promise<unknown> {
210
+ return this.request('textDocument/signatureHelp', {
211
+ textDocument: { uri },
212
+ position: { line, character },
213
+ })
214
+ }
215
+
216
+ /**
217
+ * textDocument/documentSymbol - Get document symbols
218
+ */
219
+ async documentSymbols(uri: string): Promise<unknown> {
220
+ return this.request('textDocument/documentSymbol', {
221
+ textDocument: { uri },
222
+ })
223
+ }
224
+
225
+ /**
226
+ * workspace/symbol - Search for symbols across workspace
227
+ */
228
+ async workspaceSymbols(query: string): Promise<unknown> {
229
+ return this.request('workspace/symbol', { query })
230
+ }
231
+
232
+ /**
233
+ * Open a document in the LSP server
234
+ */
235
+ openDocument(uri: string, languageId: string, version: number, text: string): void {
236
+ this.notify('textDocument/didOpen', {
237
+ textDocument: {
238
+ uri,
239
+ languageId,
240
+ version,
241
+ text,
242
+ },
243
+ })
244
+ }
245
+
246
+ /**
247
+ * Close a document in the LSP server
248
+ */
249
+ closeDocument(uri: string): void {
250
+ this.notify('textDocument/didClose', {
251
+ textDocument: { uri },
252
+ })
253
+ }
254
+
255
+ async #initialize(): Promise<void> {
256
+ const result = await this.request('initialize', {
257
+ processId: process.pid,
258
+ rootUri: this.#rootUri,
259
+ capabilities: {
260
+ textDocument: {
261
+ hover: { contentFormat: ['markdown', 'plaintext'] },
262
+ definition: { linkSupport: true },
263
+ references: {},
264
+ completion: {
265
+ completionItem: {
266
+ snippetSupport: true,
267
+ documentationFormat: ['markdown', 'plaintext'],
268
+ },
269
+ },
270
+ signatureHelp: {
271
+ signatureInformation: {
272
+ documentationFormat: ['markdown', 'plaintext'],
273
+ },
274
+ },
275
+ documentSymbol: {
276
+ hierarchicalDocumentSymbolSupport: true,
277
+ },
278
+ },
279
+ workspace: {
280
+ symbol: { symbolKind: {} },
281
+ },
282
+ },
283
+ })
284
+
285
+ this.notify('initialized', {})
286
+ this.#initialized = true
287
+
288
+ return result as Promise<void>
289
+ }
290
+
291
+ #send(message: JsonRpcRequest | JsonRpcNotification): void {
292
+ const stdin = this.#process?.stdin
293
+ if (!stdin || typeof stdin === 'number') {
294
+ throw new Error('LSP server stdin not available')
295
+ }
296
+
297
+ const content = JSON.stringify(message)
298
+ const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`
299
+
300
+ stdin.write(header + content)
301
+ }
302
+
303
+ async #readOutput(): Promise<void> {
304
+ const stdout = this.#process?.stdout
305
+ if (!stdout || typeof stdout === 'number') return
306
+
307
+ const reader = stdout.getReader()
308
+
309
+ try {
310
+ while (true) {
311
+ const { done, value } = await reader.read()
312
+ if (done) break
313
+
314
+ // Append new bytes to buffer
315
+ const newBuffer = new Uint8Array(this.#buffer.length + value.length)
316
+ newBuffer.set(this.#buffer)
317
+ newBuffer.set(value, this.#buffer.length)
318
+ this.#buffer = newBuffer
319
+
320
+ this.#processBuffer()
321
+ }
322
+ } catch {
323
+ // Stream closed
324
+ }
325
+ }
326
+
327
+ #processBuffer(): void {
328
+ const decoder = new TextDecoder()
329
+
330
+ while (true) {
331
+ // Parse header if we don't have content length yet
332
+ if (this.#contentLength === -1) {
333
+ // Look for header end sequence: \r\n\r\n
334
+ const headerEndIndex = this.#findHeaderEnd()
335
+ if (headerEndIndex === -1) break
336
+
337
+ const headerBytes = this.#buffer.slice(0, headerEndIndex)
338
+ const header = decoder.decode(headerBytes)
339
+ const match = header.match(/Content-Length: (\d+)/)
340
+ if (!match?.[1]) {
341
+ // Skip invalid header
342
+ this.#buffer = this.#buffer.slice(headerEndIndex + 4)
343
+ continue
344
+ }
345
+
346
+ this.#contentLength = parseInt(match[1], 10)
347
+ this.#buffer = this.#buffer.slice(headerEndIndex + 4)
348
+ }
349
+
350
+ // Check if we have enough content (now comparing bytes to bytes)
351
+ if (this.#buffer.length < this.#contentLength) break
352
+
353
+ const contentBytes = this.#buffer.slice(0, this.#contentLength)
354
+ const content = decoder.decode(contentBytes)
355
+ this.#buffer = this.#buffer.slice(this.#contentLength)
356
+ this.#contentLength = -1
357
+
358
+ try {
359
+ const message = JSON.parse(content) as JsonRpcResponse
360
+ this.#handleMessage(message)
361
+ } catch {
362
+ // Skip invalid JSON
363
+ }
364
+ }
365
+ }
366
+
367
+ #findHeaderEnd(): number {
368
+ // Look for \r\n\r\n sequence in buffer
369
+ const CRLF = [13, 10, 13, 10] // \r\n\r\n
370
+ for (let i = 0; i <= this.#buffer.length - 4; i++) {
371
+ if (
372
+ this.#buffer[i] === CRLF[0] &&
373
+ this.#buffer[i + 1] === CRLF[1] &&
374
+ this.#buffer[i + 2] === CRLF[2] &&
375
+ this.#buffer[i + 3] === CRLF[3]
376
+ ) {
377
+ return i
378
+ }
379
+ }
380
+ return -1
381
+ }
382
+
383
+ #handleMessage(message: JsonRpcResponse): void {
384
+ if ('id' in message && message.id !== undefined) {
385
+ const pending = this.#pendingRequests.get(message.id)
386
+ if (pending) {
387
+ if (pending.timer) {
388
+ clearTimeout(pending.timer)
389
+ }
390
+ this.#pendingRequests.delete(message.id)
391
+ if (message.error) {
392
+ pending.reject(new Error(`LSP Error: ${message.error.message}`))
393
+ } else {
394
+ pending.resolve(message.result)
395
+ }
396
+ }
397
+ }
398
+ // Notifications and server requests are ignored for now
399
+ }
400
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Search for symbols across the workspace by name
4
+ *
5
+ * Usage: bun lsp-find.ts <query> [file]
6
+ */
7
+
8
+ import { parseArgs } from 'node:util'
9
+ import { LspClient } from './lsp-client.ts'
10
+ import { resolveFilePath } from './resolve-file-path.ts'
11
+
12
+ /**
13
+ * Find a default context file when none is provided
14
+ *
15
+ * @remarks
16
+ * Checks common TypeScript entry points in order of preference
17
+ */
18
+ const findDefaultContextFile = async (): Promise<string | null> => {
19
+ const candidates = [`${process.cwd()}/src/index.ts`, `${process.cwd()}/src/main.ts`, `${process.cwd()}/index.ts`]
20
+ for (const candidate of candidates) {
21
+ if (await Bun.file(candidate).exists()) {
22
+ return candidate
23
+ }
24
+ }
25
+ return null
26
+ }
27
+
28
+ /**
29
+ * Search for symbols across the workspace by name
30
+ *
31
+ * @param args - Command line arguments [query, file?]
32
+ */
33
+ export const lspFind = async (args: string[]) => {
34
+ const { positionals } = parseArgs({
35
+ args,
36
+ allowPositionals: true,
37
+ })
38
+
39
+ const [query, filePath] = positionals
40
+
41
+ if (!query) {
42
+ console.error('Usage: lsp-find <query> [file]')
43
+ console.error(' query: Symbol name or partial name to search')
44
+ console.error(' file: Optional file to open for project context')
45
+ process.exit(1)
46
+ }
47
+
48
+ const rootUri = `file://${process.cwd()}`
49
+ const client = new LspClient({ rootUri })
50
+
51
+ try {
52
+ await client.start()
53
+
54
+ // Open a file to establish project context if provided, otherwise find a default
55
+ const contextFile = filePath ? await resolveFilePath(filePath) : await findDefaultContextFile()
56
+
57
+ if (!contextFile) {
58
+ console.error('Error: No context file found.')
59
+ console.error('Provide a file path or ensure src/index.ts, src/main.ts, or index.ts exists.')
60
+ await client.stop()
61
+ process.exit(1)
62
+ }
63
+
64
+ const file = Bun.file(contextFile)
65
+ if (!(await file.exists())) {
66
+ console.error(`Error: Context file not found: ${contextFile}`)
67
+ console.error('Workspace symbol search requires at least one open document.')
68
+ await client.stop()
69
+ process.exit(1)
70
+ }
71
+
72
+ const text = await file.text()
73
+ const uri = `file://${contextFile}`
74
+ const languageId = contextFile.endsWith('.tsx')
75
+ ? 'typescriptreact'
76
+ : contextFile.endsWith('.ts')
77
+ ? 'typescript'
78
+ : contextFile.endsWith('.jsx')
79
+ ? 'javascriptreact'
80
+ : 'javascript'
81
+
82
+ client.openDocument(uri, languageId, 1, text)
83
+
84
+ const result = await client.workspaceSymbols(query)
85
+
86
+ client.closeDocument(uri)
87
+ await client.stop()
88
+
89
+ console.log(JSON.stringify(result, null, 2))
90
+ } catch (error) {
91
+ console.error(`Error: ${error}`)
92
+ await client.stop()
93
+ process.exit(1)
94
+ }
95
+ }
96
+
97
+ // Keep executable entry point for direct execution
98
+ if (import.meta.main) {
99
+ await lspFind(Bun.argv.slice(2))
100
+ }