wogiflow 1.0.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.
- package/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flow-lsp.js - LSP Client for Wogi Flow
|
|
4
|
+
*
|
|
5
|
+
* Provides Language Server Protocol integration for:
|
|
6
|
+
* - Type information at cursor position
|
|
7
|
+
* - Diagnostics (errors/warnings)
|
|
8
|
+
* - Go to definition
|
|
9
|
+
* - Completions
|
|
10
|
+
*
|
|
11
|
+
* Used by hybrid mode to get accurate type info instead of guessing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { spawn, spawnSync } = require('child_process');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { getConfig, PROJECT_ROOT, colors, success, warn, error } = require('./flow-utils');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a file path to a proper file:// URI
|
|
21
|
+
* Handles Windows paths correctly (file:///C:/path vs file:///home/user)
|
|
22
|
+
*/
|
|
23
|
+
function pathToFileUri(filePath) {
|
|
24
|
+
// Normalize to forward slashes
|
|
25
|
+
let normalized = filePath.replace(/\\/g, '/');
|
|
26
|
+
|
|
27
|
+
// On Windows, paths like C:/foo need an extra slash: file:///C:/foo
|
|
28
|
+
// On Unix, paths like /home/foo just need file:///home/foo
|
|
29
|
+
if (/^[a-zA-Z]:/.test(normalized)) {
|
|
30
|
+
// Windows path with drive letter
|
|
31
|
+
return `file:///${normalized}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Unix path - already starts with /
|
|
35
|
+
return `file://${normalized}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
// LSP Client Class
|
|
40
|
+
// ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
class LSPClient {
|
|
43
|
+
constructor(projectRoot) {
|
|
44
|
+
this.projectRoot = projectRoot;
|
|
45
|
+
this.process = null;
|
|
46
|
+
this.requestId = 0;
|
|
47
|
+
this.pending = new Map();
|
|
48
|
+
this.initialized = false;
|
|
49
|
+
this.buffer = '';
|
|
50
|
+
this.diagnosticsCache = new Map();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start the LSP server
|
|
55
|
+
*/
|
|
56
|
+
async start() {
|
|
57
|
+
const config = getConfig();
|
|
58
|
+
const serverCommand = config.lsp?.server || 'typescript-language-server';
|
|
59
|
+
|
|
60
|
+
// Find the language server
|
|
61
|
+
const tsserver = this._findServer(serverCommand);
|
|
62
|
+
if (!tsserver) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`${serverCommand} not found. Install with:\n` +
|
|
65
|
+
' npm i -g typescript-language-server typescript\n' +
|
|
66
|
+
'Or for local install:\n' +
|
|
67
|
+
' npm i -D typescript-language-server typescript'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Spawn the server
|
|
72
|
+
this.process = spawn(tsserver, ['--stdio'], {
|
|
73
|
+
cwd: this.projectRoot,
|
|
74
|
+
env: { ...process.env },
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Set up I/O handling
|
|
79
|
+
this._setupIO();
|
|
80
|
+
|
|
81
|
+
// Initialize the connection
|
|
82
|
+
await this._initialize();
|
|
83
|
+
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Find the language server executable
|
|
89
|
+
*/
|
|
90
|
+
_findServer(serverCommand) {
|
|
91
|
+
// Check common locations
|
|
92
|
+
const locations = [
|
|
93
|
+
serverCommand,
|
|
94
|
+
path.join(this.projectRoot, 'node_modules/.bin', serverCommand),
|
|
95
|
+
path.join(this.projectRoot, 'node_modules/.bin/typescript-language-server'),
|
|
96
|
+
'/usr/local/bin/typescript-language-server'
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const loc of locations) {
|
|
100
|
+
try {
|
|
101
|
+
const result = spawnSync('which', [loc], { encoding: 'utf-8' });
|
|
102
|
+
if (result.status === 0) {
|
|
103
|
+
return result.stdout.trim() || loc;
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Try next location
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Also try direct execution check
|
|
110
|
+
try {
|
|
111
|
+
const result = spawnSync(loc, ['--version'], { encoding: 'utf-8', timeout: 5000 });
|
|
112
|
+
if (result.status === 0) {
|
|
113
|
+
return loc;
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// Try next location
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set up stdin/stdout handling for LSP protocol
|
|
125
|
+
*/
|
|
126
|
+
_setupIO() {
|
|
127
|
+
this.process.stdout.on('data', (data) => {
|
|
128
|
+
this.buffer += data.toString();
|
|
129
|
+
this._parseMessages();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.process.stderr.on('data', (data) => {
|
|
133
|
+
// Log stderr but don't crash
|
|
134
|
+
if (process.env.DEBUG_LSP) {
|
|
135
|
+
console.error('[LSP stderr]', data.toString());
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.process.on('error', (err) => {
|
|
140
|
+
error(`LSP process error: ${err.message}`);
|
|
141
|
+
this.initialized = false;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.process.on('exit', (code) => {
|
|
145
|
+
if (code !== 0 && process.env.DEBUG_LSP) {
|
|
146
|
+
console.error(`[LSP] Process exited with code ${code}`);
|
|
147
|
+
}
|
|
148
|
+
this.initialized = false;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse LSP messages from buffer
|
|
154
|
+
*/
|
|
155
|
+
_parseMessages() {
|
|
156
|
+
while (true) {
|
|
157
|
+
// Look for Content-Length header
|
|
158
|
+
const headerMatch = this.buffer.match(/Content-Length: (\d+)\r\n\r\n/);
|
|
159
|
+
if (!headerMatch) break;
|
|
160
|
+
|
|
161
|
+
const contentLength = parseInt(headerMatch[1], 10);
|
|
162
|
+
const headerEnd = headerMatch.index + headerMatch[0].length;
|
|
163
|
+
|
|
164
|
+
// Check if we have the full message
|
|
165
|
+
if (this.buffer.length < headerEnd + contentLength) break;
|
|
166
|
+
|
|
167
|
+
// Extract the message
|
|
168
|
+
const messageStr = this.buffer.slice(headerEnd, headerEnd + contentLength);
|
|
169
|
+
this.buffer = this.buffer.slice(headerEnd + contentLength);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const message = JSON.parse(messageStr);
|
|
173
|
+
this._handleMessage(message);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (process.env.DEBUG_LSP) {
|
|
176
|
+
console.error('[LSP] Failed to parse message:', err.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle incoming LSP message
|
|
184
|
+
*/
|
|
185
|
+
_handleMessage(msg) {
|
|
186
|
+
// Response to a request
|
|
187
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
188
|
+
const { resolve, reject, timeout } = this.pending.get(msg.id);
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
this.pending.delete(msg.id);
|
|
191
|
+
|
|
192
|
+
if (msg.error) {
|
|
193
|
+
reject(new Error(msg.error.message || 'LSP error'));
|
|
194
|
+
} else {
|
|
195
|
+
resolve(msg.result);
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Notification (e.g., publishDiagnostics)
|
|
201
|
+
if (msg.method === 'textDocument/publishDiagnostics') {
|
|
202
|
+
this._handleDiagnostics(msg.params);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Handle diagnostics notification
|
|
208
|
+
*/
|
|
209
|
+
_handleDiagnostics(params) {
|
|
210
|
+
const uri = params.uri;
|
|
211
|
+
const diagnostics = params.diagnostics || [];
|
|
212
|
+
this.diagnosticsCache.set(uri, diagnostics);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Send a request to the LSP server
|
|
217
|
+
*/
|
|
218
|
+
_send(method, params) {
|
|
219
|
+
const config = getConfig();
|
|
220
|
+
const timeout = config.lsp?.timeout || 5000;
|
|
221
|
+
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const id = ++this.requestId;
|
|
224
|
+
|
|
225
|
+
// Set up timeout
|
|
226
|
+
const timeoutId = setTimeout(() => {
|
|
227
|
+
if (this.pending.has(id)) {
|
|
228
|
+
this.pending.delete(id);
|
|
229
|
+
reject(new Error(`LSP request timeout: ${method}`));
|
|
230
|
+
}
|
|
231
|
+
}, timeout);
|
|
232
|
+
|
|
233
|
+
this.pending.set(id, { resolve, reject, timeout: timeoutId });
|
|
234
|
+
|
|
235
|
+
const message = JSON.stringify({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id,
|
|
238
|
+
method,
|
|
239
|
+
params
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
|
|
243
|
+
this.process.stdin.write(header + message);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Send a notification (no response expected)
|
|
249
|
+
*/
|
|
250
|
+
_notify(method, params) {
|
|
251
|
+
const message = JSON.stringify({
|
|
252
|
+
jsonrpc: '2.0',
|
|
253
|
+
method,
|
|
254
|
+
params
|
|
255
|
+
});
|
|
256
|
+
const header = `Content-Length: ${Buffer.byteLength(message)}\r\n\r\n`;
|
|
257
|
+
this.process.stdin.write(header + message);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Initialize the LSP connection
|
|
262
|
+
*/
|
|
263
|
+
async _initialize() {
|
|
264
|
+
const result = await this._send('initialize', {
|
|
265
|
+
processId: process.pid,
|
|
266
|
+
rootUri: pathToFileUri(this.projectRoot),
|
|
267
|
+
rootPath: this.projectRoot,
|
|
268
|
+
capabilities: {
|
|
269
|
+
textDocument: {
|
|
270
|
+
hover: {
|
|
271
|
+
contentFormat: ['markdown', 'plaintext']
|
|
272
|
+
},
|
|
273
|
+
completion: {
|
|
274
|
+
completionItem: {
|
|
275
|
+
snippetSupport: true,
|
|
276
|
+
documentationFormat: ['markdown', 'plaintext']
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
synchronization: {
|
|
280
|
+
didSave: true,
|
|
281
|
+
willSave: false,
|
|
282
|
+
willSaveWaitUntil: false
|
|
283
|
+
},
|
|
284
|
+
publishDiagnostics: {
|
|
285
|
+
relatedInformation: true
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
workspace: {
|
|
289
|
+
workspaceFolders: true
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
workspaceFolders: [
|
|
293
|
+
{ uri: pathToFileUri(this.projectRoot), name: path.basename(this.projectRoot) }
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Send initialized notification
|
|
298
|
+
this._notify('initialized', {});
|
|
299
|
+
|
|
300
|
+
this.initialized = true;
|
|
301
|
+
this.serverCapabilities = result?.capabilities || {};
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Open a document (required before querying)
|
|
308
|
+
*/
|
|
309
|
+
async openDocument(filePath) {
|
|
310
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectRoot, filePath);
|
|
311
|
+
const uri = pathToFileUri(absPath);
|
|
312
|
+
|
|
313
|
+
if (!fs.existsSync(absPath)) {
|
|
314
|
+
throw new Error(`File not found: ${absPath}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
318
|
+
const languageId = this._getLanguageId(absPath);
|
|
319
|
+
|
|
320
|
+
this._notify('textDocument/didOpen', {
|
|
321
|
+
textDocument: {
|
|
322
|
+
uri,
|
|
323
|
+
languageId,
|
|
324
|
+
version: 1,
|
|
325
|
+
text: content
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Wait a bit for the server to process
|
|
330
|
+
await new Promise(r => setTimeout(r, 100));
|
|
331
|
+
|
|
332
|
+
return uri;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Close a document
|
|
337
|
+
*/
|
|
338
|
+
closeDocument(uri) {
|
|
339
|
+
this._notify('textDocument/didClose', {
|
|
340
|
+
textDocument: { uri }
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get language ID from file extension
|
|
346
|
+
*/
|
|
347
|
+
_getLanguageId(filePath) {
|
|
348
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
349
|
+
const map = {
|
|
350
|
+
'.ts': 'typescript',
|
|
351
|
+
'.tsx': 'typescriptreact',
|
|
352
|
+
'.js': 'javascript',
|
|
353
|
+
'.jsx': 'javascriptreact',
|
|
354
|
+
'.mjs': 'javascript',
|
|
355
|
+
'.cjs': 'javascript',
|
|
356
|
+
'.json': 'json'
|
|
357
|
+
};
|
|
358
|
+
return map[ext] || 'typescript';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ─────────────────────────────────────────────────────────────
|
|
362
|
+
// Public API
|
|
363
|
+
// ─────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get hover information at a position
|
|
367
|
+
* @param {string} filePath - Path to file
|
|
368
|
+
* @param {number} line - 0-indexed line number
|
|
369
|
+
* @param {number} character - 0-indexed character position
|
|
370
|
+
*/
|
|
371
|
+
async hover(filePath, line, character) {
|
|
372
|
+
const uri = await this.openDocument(filePath);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const result = await this._send('textDocument/hover', {
|
|
376
|
+
textDocument: { uri },
|
|
377
|
+
position: { line, character }
|
|
378
|
+
});
|
|
379
|
+
return result;
|
|
380
|
+
} finally {
|
|
381
|
+
this.closeDocument(uri);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get type at a specific position
|
|
387
|
+
* @returns {string|null} Type signature or null if not available
|
|
388
|
+
*/
|
|
389
|
+
async getTypeAtPosition(filePath, line, character) {
|
|
390
|
+
const hover = await this.hover(filePath, line, character);
|
|
391
|
+
if (!hover?.contents) return null;
|
|
392
|
+
|
|
393
|
+
// Extract type from hover content
|
|
394
|
+
const content = typeof hover.contents === 'string'
|
|
395
|
+
? hover.contents
|
|
396
|
+
: hover.contents.value || '';
|
|
397
|
+
|
|
398
|
+
// Parse TypeScript type signature from markdown code block
|
|
399
|
+
const codeBlockMatch = content.match(/```(?:typescript|ts)\n([\s\S]*?)\n```/);
|
|
400
|
+
if (codeBlockMatch) {
|
|
401
|
+
return codeBlockMatch[1].trim();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Try to find type in plain format
|
|
405
|
+
const typeMatch = content.match(/^(\w+):\s*(.+)$/m);
|
|
406
|
+
if (typeMatch) {
|
|
407
|
+
return `${typeMatch[1]}: ${typeMatch[2]}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return content.trim() || null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get diagnostics for a file
|
|
415
|
+
* @param {string} filePath - Path to file
|
|
416
|
+
* @returns {Array} Array of diagnostic objects
|
|
417
|
+
*/
|
|
418
|
+
async getDiagnostics(filePath) {
|
|
419
|
+
const uri = await this.openDocument(filePath);
|
|
420
|
+
|
|
421
|
+
// Wait for diagnostics to be pushed
|
|
422
|
+
await new Promise(r => setTimeout(r, 500));
|
|
423
|
+
|
|
424
|
+
const diagnostics = this.diagnosticsCache.get(uri) || [];
|
|
425
|
+
|
|
426
|
+
this.closeDocument(uri);
|
|
427
|
+
|
|
428
|
+
return diagnostics.map(d => ({
|
|
429
|
+
severity: this._diagnosticSeverity(d.severity),
|
|
430
|
+
message: d.message,
|
|
431
|
+
line: d.range?.start?.line,
|
|
432
|
+
character: d.range?.start?.character,
|
|
433
|
+
source: d.source,
|
|
434
|
+
code: d.code
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Convert diagnostic severity to string
|
|
440
|
+
*/
|
|
441
|
+
_diagnosticSeverity(severity) {
|
|
442
|
+
const map = { 1: 'error', 2: 'warning', 3: 'info', 4: 'hint' };
|
|
443
|
+
return map[severity] || 'unknown';
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get completions at a position
|
|
448
|
+
* @param {string} filePath - Path to file
|
|
449
|
+
* @param {number} line - 0-indexed line number
|
|
450
|
+
* @param {number} character - 0-indexed character position
|
|
451
|
+
*/
|
|
452
|
+
async getCompletions(filePath, line, character) {
|
|
453
|
+
const uri = await this.openDocument(filePath);
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const result = await this._send('textDocument/completion', {
|
|
457
|
+
textDocument: { uri },
|
|
458
|
+
position: { line, character }
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const items = Array.isArray(result) ? result : (result?.items || []);
|
|
462
|
+
|
|
463
|
+
return items.map(item => ({
|
|
464
|
+
label: item.label,
|
|
465
|
+
kind: this._completionKind(item.kind),
|
|
466
|
+
detail: item.detail,
|
|
467
|
+
documentation: typeof item.documentation === 'string'
|
|
468
|
+
? item.documentation
|
|
469
|
+
: item.documentation?.value
|
|
470
|
+
}));
|
|
471
|
+
} finally {
|
|
472
|
+
this.closeDocument(uri);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Convert completion kind to string
|
|
478
|
+
*/
|
|
479
|
+
_completionKind(kind) {
|
|
480
|
+
const map = {
|
|
481
|
+
1: 'text', 2: 'method', 3: 'function', 4: 'constructor',
|
|
482
|
+
5: 'field', 6: 'variable', 7: 'class', 8: 'interface',
|
|
483
|
+
9: 'module', 10: 'property', 11: 'unit', 12: 'value',
|
|
484
|
+
13: 'enum', 14: 'keyword', 15: 'snippet', 16: 'color',
|
|
485
|
+
17: 'file', 18: 'reference', 19: 'folder', 20: 'enumMember',
|
|
486
|
+
21: 'constant', 22: 'struct', 23: 'event', 24: 'operator',
|
|
487
|
+
25: 'typeParameter'
|
|
488
|
+
};
|
|
489
|
+
return map[kind] || 'unknown';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Go to definition
|
|
494
|
+
* @param {string} filePath - Path to file
|
|
495
|
+
* @param {number} line - 0-indexed line number
|
|
496
|
+
* @param {number} character - 0-indexed character position
|
|
497
|
+
*/
|
|
498
|
+
async getDefinition(filePath, line, character) {
|
|
499
|
+
const uri = await this.openDocument(filePath);
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const result = await this._send('textDocument/definition', {
|
|
503
|
+
textDocument: { uri },
|
|
504
|
+
position: { line, character }
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const locations = Array.isArray(result) ? result : (result ? [result] : []);
|
|
508
|
+
|
|
509
|
+
return locations.map(loc => ({
|
|
510
|
+
uri: loc.uri || loc.targetUri,
|
|
511
|
+
path: (loc.uri || loc.targetUri)?.replace('file://', ''),
|
|
512
|
+
range: loc.range || loc.targetRange
|
|
513
|
+
}));
|
|
514
|
+
} finally {
|
|
515
|
+
this.closeDocument(uri);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Get document symbols (functions, classes, etc.)
|
|
521
|
+
* @param {string} filePath - Path to file
|
|
522
|
+
*/
|
|
523
|
+
async getDocumentSymbols(filePath) {
|
|
524
|
+
const uri = await this.openDocument(filePath);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const result = await this._send('textDocument/documentSymbol', {
|
|
528
|
+
textDocument: { uri }
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return this._flattenSymbols(result || []);
|
|
532
|
+
} finally {
|
|
533
|
+
this.closeDocument(uri);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Flatten hierarchical symbols
|
|
539
|
+
*/
|
|
540
|
+
_flattenSymbols(symbols, parent = null) {
|
|
541
|
+
const flat = [];
|
|
542
|
+
for (const sym of symbols) {
|
|
543
|
+
flat.push({
|
|
544
|
+
name: sym.name,
|
|
545
|
+
kind: this._symbolKind(sym.kind),
|
|
546
|
+
parent: parent?.name,
|
|
547
|
+
range: sym.range || sym.location?.range,
|
|
548
|
+
selectionRange: sym.selectionRange
|
|
549
|
+
});
|
|
550
|
+
if (sym.children) {
|
|
551
|
+
flat.push(...this._flattenSymbols(sym.children, sym));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return flat;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Convert symbol kind to string
|
|
559
|
+
*/
|
|
560
|
+
_symbolKind(kind) {
|
|
561
|
+
const map = {
|
|
562
|
+
1: 'file', 2: 'module', 3: 'namespace', 4: 'package',
|
|
563
|
+
5: 'class', 6: 'method', 7: 'property', 8: 'field',
|
|
564
|
+
9: 'constructor', 10: 'enum', 11: 'interface', 12: 'function',
|
|
565
|
+
13: 'variable', 14: 'constant', 15: 'string', 16: 'number',
|
|
566
|
+
17: 'boolean', 18: 'array', 19: 'object', 20: 'key',
|
|
567
|
+
21: 'null', 22: 'enumMember', 23: 'struct', 24: 'event',
|
|
568
|
+
25: 'operator', 26: 'typeParameter'
|
|
569
|
+
};
|
|
570
|
+
return map[kind] || 'unknown';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Stop the LSP server
|
|
575
|
+
*/
|
|
576
|
+
async stop() {
|
|
577
|
+
if (!this.process) return;
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
await this._send('shutdown', null);
|
|
581
|
+
this._notify('exit', null);
|
|
582
|
+
} catch (err) {
|
|
583
|
+
// Ignore errors during shutdown
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Force kill if still running after 1 second
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
if (this.process && !this.process.killed) {
|
|
589
|
+
this.process.kill('SIGKILL');
|
|
590
|
+
}
|
|
591
|
+
}, 1000);
|
|
592
|
+
|
|
593
|
+
this.process = null;
|
|
594
|
+
this.initialized = false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ─────────────────────────────────────────────────────────────
|
|
599
|
+
// Singleton Manager
|
|
600
|
+
// ─────────────────────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
let instance = null;
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get or create LSP client instance
|
|
606
|
+
* @param {string} projectRoot - Project root directory
|
|
607
|
+
*/
|
|
608
|
+
async function getLSP(projectRoot = PROJECT_ROOT) {
|
|
609
|
+
const config = getConfig();
|
|
610
|
+
|
|
611
|
+
// Return null if LSP is disabled
|
|
612
|
+
if (!config.lsp?.enabled) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Reuse existing instance if same project
|
|
617
|
+
if (instance && instance.projectRoot === projectRoot && instance.initialized) {
|
|
618
|
+
return instance;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Clean up old instance
|
|
622
|
+
if (instance) {
|
|
623
|
+
await instance.stop();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Create new instance
|
|
627
|
+
try {
|
|
628
|
+
instance = new LSPClient(projectRoot);
|
|
629
|
+
await instance.start();
|
|
630
|
+
return instance;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
warn(`LSP initialization failed: ${err.message}`);
|
|
633
|
+
instance = null;
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Check if LSP is available and enabled
|
|
640
|
+
*/
|
|
641
|
+
function isLSPEnabled() {
|
|
642
|
+
const config = getConfig();
|
|
643
|
+
return config.lsp?.enabled === true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Stop the LSP server
|
|
648
|
+
*/
|
|
649
|
+
async function stopLSP() {
|
|
650
|
+
if (instance) {
|
|
651
|
+
await instance.stop();
|
|
652
|
+
instance = null;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ─────────────────────────────────────────────────────────────
|
|
657
|
+
// High-Level Helpers
|
|
658
|
+
// ─────────────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Get types for multiple positions in a file
|
|
662
|
+
* @param {string} filePath - Path to file
|
|
663
|
+
* @param {Array<{line: number, character: number, name?: string}>} positions
|
|
664
|
+
* @returns {Object} Map of name/position to type
|
|
665
|
+
*/
|
|
666
|
+
async function getTypesForPositions(filePath, positions) {
|
|
667
|
+
const lsp = await getLSP();
|
|
668
|
+
if (!lsp) return {};
|
|
669
|
+
|
|
670
|
+
const types = {};
|
|
671
|
+
const uri = await lsp.openDocument(filePath);
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
for (const pos of positions) {
|
|
675
|
+
try {
|
|
676
|
+
const hover = await lsp._send('textDocument/hover', {
|
|
677
|
+
textDocument: { uri },
|
|
678
|
+
position: { line: pos.line, character: pos.character }
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
if (hover?.contents) {
|
|
682
|
+
const content = typeof hover.contents === 'string'
|
|
683
|
+
? hover.contents
|
|
684
|
+
: hover.contents.value || '';
|
|
685
|
+
|
|
686
|
+
const key = pos.name || `${pos.line}:${pos.character}`;
|
|
687
|
+
types[key] = extractTypeFromHover(content);
|
|
688
|
+
}
|
|
689
|
+
} catch (err) {
|
|
690
|
+
// Skip individual errors
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} finally {
|
|
694
|
+
lsp.closeDocument(uri);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return types;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Extract type signature from hover content
|
|
702
|
+
*/
|
|
703
|
+
function extractTypeFromHover(content) {
|
|
704
|
+
// Try code block first
|
|
705
|
+
const codeMatch = content.match(/```(?:typescript|ts)\n([\s\S]*?)\n```/);
|
|
706
|
+
if (codeMatch) {
|
|
707
|
+
return codeMatch[1].trim();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Try inline code
|
|
711
|
+
const inlineMatch = content.match(/`([^`]+)`/);
|
|
712
|
+
if (inlineMatch) {
|
|
713
|
+
return inlineMatch[1];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return content.trim();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Validate a file and get all errors
|
|
721
|
+
* @param {string} filePath - Path to file
|
|
722
|
+
* @returns {Array} Array of errors
|
|
723
|
+
*/
|
|
724
|
+
async function validateFile(filePath) {
|
|
725
|
+
const lsp = await getLSP();
|
|
726
|
+
if (!lsp) return [];
|
|
727
|
+
|
|
728
|
+
return lsp.getDiagnostics(filePath);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Get function/method signature at cursor
|
|
733
|
+
* @param {string} filePath - Path to file
|
|
734
|
+
* @param {number} line - Line number
|
|
735
|
+
* @param {number} character - Character position
|
|
736
|
+
*/
|
|
737
|
+
async function getSignatureAtPosition(filePath, line, character) {
|
|
738
|
+
const lsp = await getLSP();
|
|
739
|
+
if (!lsp) return null;
|
|
740
|
+
|
|
741
|
+
return lsp.getTypeAtPosition(filePath, line, character);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// ─────────────────────────────────────────────────────────────
|
|
745
|
+
// CLI Interface
|
|
746
|
+
// ─────────────────────────────────────────────────────────────
|
|
747
|
+
|
|
748
|
+
async function main() {
|
|
749
|
+
const args = process.argv.slice(2);
|
|
750
|
+
|
|
751
|
+
if (args.length === 0 || args[0] === '--help') {
|
|
752
|
+
console.log(`
|
|
753
|
+
${colors.bold}Wogi Flow LSP Client${colors.reset}
|
|
754
|
+
|
|
755
|
+
Usage: flow-lsp.js <command> [options]
|
|
756
|
+
|
|
757
|
+
Commands:
|
|
758
|
+
hover <file> <line> <char> Get type info at position
|
|
759
|
+
diagnostics <file> Get file diagnostics
|
|
760
|
+
symbols <file> Get document symbols
|
|
761
|
+
definition <file> <l> <c> Go to definition
|
|
762
|
+
test Test LSP connection
|
|
763
|
+
|
|
764
|
+
Examples:
|
|
765
|
+
flow-lsp.js hover src/index.ts 10 5
|
|
766
|
+
flow-lsp.js diagnostics src/index.ts
|
|
767
|
+
flow-lsp.js test
|
|
768
|
+
`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const command = args[0];
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
switch (command) {
|
|
776
|
+
case 'hover': {
|
|
777
|
+
const [, file, line, char] = args;
|
|
778
|
+
if (!file || line === undefined || char === undefined) {
|
|
779
|
+
error('Usage: hover <file> <line> <char>');
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
const lsp = await getLSP();
|
|
783
|
+
if (!lsp) {
|
|
784
|
+
error('LSP not enabled. Set lsp.enabled: true in config.json');
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const result = await lsp.hover(file, parseInt(line), parseInt(char));
|
|
788
|
+
console.log(JSON.stringify(result, null, 2));
|
|
789
|
+
await stopLSP();
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
case 'type': {
|
|
794
|
+
const [, file, line, char] = args;
|
|
795
|
+
if (!file || line === undefined || char === undefined) {
|
|
796
|
+
error('Usage: type <file> <line> <char>');
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
const lsp = await getLSP();
|
|
800
|
+
if (!lsp) {
|
|
801
|
+
error('LSP not enabled');
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
const type = await lsp.getTypeAtPosition(file, parseInt(line), parseInt(char));
|
|
805
|
+
console.log(type || '(no type info)');
|
|
806
|
+
await stopLSP();
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
case 'diagnostics': {
|
|
811
|
+
const [, file] = args;
|
|
812
|
+
if (!file) {
|
|
813
|
+
error('Usage: diagnostics <file>');
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
const lsp = await getLSP();
|
|
817
|
+
if (!lsp) {
|
|
818
|
+
error('LSP not enabled');
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
const diags = await lsp.getDiagnostics(file);
|
|
822
|
+
console.log(JSON.stringify(diags, null, 2));
|
|
823
|
+
await stopLSP();
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
case 'symbols': {
|
|
828
|
+
const [, file] = args;
|
|
829
|
+
if (!file) {
|
|
830
|
+
error('Usage: symbols <file>');
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const lsp = await getLSP();
|
|
834
|
+
if (!lsp) {
|
|
835
|
+
error('LSP not enabled');
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
const symbols = await lsp.getDocumentSymbols(file);
|
|
839
|
+
console.log(JSON.stringify(symbols, null, 2));
|
|
840
|
+
await stopLSP();
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
case 'definition': {
|
|
845
|
+
const [, file, line, char] = args;
|
|
846
|
+
if (!file || line === undefined || char === undefined) {
|
|
847
|
+
error('Usage: definition <file> <line> <char>');
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
const lsp = await getLSP();
|
|
851
|
+
if (!lsp) {
|
|
852
|
+
error('LSP not enabled');
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
const defs = await lsp.getDefinition(file, parseInt(line), parseInt(char));
|
|
856
|
+
console.log(JSON.stringify(defs, null, 2));
|
|
857
|
+
await stopLSP();
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
case 'test': {
|
|
862
|
+
console.log(`${colors.cyan}Testing LSP connection...${colors.reset}\n`);
|
|
863
|
+
|
|
864
|
+
const config = getConfig();
|
|
865
|
+
console.log(`LSP enabled: ${config.lsp?.enabled ? 'yes' : 'no'}`);
|
|
866
|
+
console.log(`Server: ${config.lsp?.server || 'typescript-language-server'}`);
|
|
867
|
+
console.log(`Timeout: ${config.lsp?.timeout || 5000}ms\n`);
|
|
868
|
+
|
|
869
|
+
if (!config.lsp?.enabled) {
|
|
870
|
+
warn('LSP is disabled. Enable with: lsp.enabled: true in config.json');
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
const lsp = await getLSP();
|
|
876
|
+
if (lsp) {
|
|
877
|
+
success('LSP server started successfully');
|
|
878
|
+
console.log(`Server capabilities: ${Object.keys(lsp.serverCapabilities || {}).length} features`);
|
|
879
|
+
await stopLSP();
|
|
880
|
+
}
|
|
881
|
+
} catch (err) {
|
|
882
|
+
error(`LSP test failed: ${err.message}`);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
default:
|
|
889
|
+
error(`Unknown command: ${command}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
} catch (err) {
|
|
893
|
+
error(`Error: ${err.message}`);
|
|
894
|
+
await stopLSP();
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ─────────────────────────────────────────────────────────────
|
|
900
|
+
// Exports
|
|
901
|
+
// ─────────────────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
module.exports = {
|
|
904
|
+
// Main API
|
|
905
|
+
getLSP,
|
|
906
|
+
stopLSP,
|
|
907
|
+
isLSPEnabled,
|
|
908
|
+
|
|
909
|
+
// High-level helpers
|
|
910
|
+
getTypesForPositions,
|
|
911
|
+
validateFile,
|
|
912
|
+
getSignatureAtPosition,
|
|
913
|
+
|
|
914
|
+
// Low-level access
|
|
915
|
+
LSPClient
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
if (require.main === module) {
|
|
919
|
+
main().catch(e => {
|
|
920
|
+
error(err.message);
|
|
921
|
+
process.exit(1);
|
|
922
|
+
});
|
|
923
|
+
}
|