march-cli 0.1.9 → 0.1.10
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/package.json +1 -1
- package/src/agent/editing/lsp-report.mjs +69 -0
- package/src/agent/file-edit-tool.mjs +10 -24
- package/src/agent/model-payload-dumper.mjs +11 -4
- package/src/agent/runner/runner-utils.mjs +18 -0
- package/src/agent/runner.mjs +26 -27
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/turn/turn-logging.mjs +30 -0
- package/src/agent/turn/turn-runner.mjs +40 -0
- package/src/cli/commands/status-command.mjs +3 -4
- package/src/cli/permissions.mjs +1 -1
- package/src/cli/startup/runtime-close.mjs +23 -0
- package/src/cli/tui/tool-rendering.mjs +1 -1
- package/src/config/loader.mjs +28 -1
- package/src/debug/logger.mjs +141 -0
- package/src/lsp/client.mjs +2 -2
- package/src/lsp/diagnostic-store.mjs +5 -2
- package/src/lsp/diagnostics-format.mjs +3 -1
- package/src/lsp/managed-node-server.mjs +94 -0
- package/src/lsp/path-match.mjs +10 -0
- package/src/lsp/servers.mjs +56 -13
- package/src/lsp/service.mjs +6 -6
- package/src/lsp/status-message.mjs +1 -0
- package/src/lsp/typescript-project-resolver.mjs +186 -0
- package/src/main.mjs +17 -24
- package/src/platform/spawn-command.mjs +27 -0
- package/src/provider/hosted-tools.mjs +111 -0
- package/src/web/tools.mjs +2 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const LEVELS = Object.freeze({ debug: 10, info: 20, warn: 30, error: 40, silent: 99 });
|
|
6
|
+
const REDACTED = "[redacted]";
|
|
7
|
+
const MAX_STRING_LENGTH = 2000;
|
|
8
|
+
const MAX_ARRAY_LENGTH = 50;
|
|
9
|
+
const MAX_OBJECT_KEYS = 80;
|
|
10
|
+
const SENSITIVE_KEY = /(api[-_]?key|authorization|auth|token|secret|password|cookie|credential|b64|base64|image)/i;
|
|
11
|
+
|
|
12
|
+
export function createLogger({
|
|
13
|
+
enabled = process.env.MARCH_LOG !== "0",
|
|
14
|
+
level = process.env.MARCH_LOG_LEVEL ?? "info",
|
|
15
|
+
logDir = defaultLogDir(),
|
|
16
|
+
now = () => new Date(),
|
|
17
|
+
pid = process.pid,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const threshold = normalizeLevel(level);
|
|
20
|
+
const path = join(logDir, `${dateStamp(now())}-march-${pid}.jsonl`);
|
|
21
|
+
const base = { enabled: Boolean(enabled), level: levelName(threshold), path };
|
|
22
|
+
|
|
23
|
+
function write(levelNameValue, event, fields = {}) {
|
|
24
|
+
if (!base.enabled || normalizeLevel(levelNameValue) < threshold) return;
|
|
25
|
+
const entry = {
|
|
26
|
+
ts: now().toISOString(),
|
|
27
|
+
level: levelNameValue,
|
|
28
|
+
event,
|
|
29
|
+
pid,
|
|
30
|
+
...sanitize(fields),
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(logDir, { recursive: true });
|
|
34
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
// Logging must never change CLI behavior.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logger = {
|
|
41
|
+
...base,
|
|
42
|
+
event: (event, fields) => write("info", event, fields),
|
|
43
|
+
debug: (event, fields) => write("debug", event, fields),
|
|
44
|
+
warn: (event, fields) => write("warn", event, fields),
|
|
45
|
+
error: (event, fields) => write("error", event, fields),
|
|
46
|
+
child(extraFields = {}) {
|
|
47
|
+
return createChildLogger(logger, sanitize(extraFields));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return logger;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createHeartbeat({ logger, event = "heartbeat", intervalMs = 10_000, getFields = () => ({}) } = {}) {
|
|
54
|
+
if (!logger?.enabled || intervalMs <= 0) return { stop() {} };
|
|
55
|
+
const timer = setInterval(() => logger.event(event, getFields()), intervalMs);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
return {
|
|
58
|
+
stop() { clearInterval(timer); },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function installProcessLogHandlers(logger) {
|
|
63
|
+
if (!logger?.enabled) return;
|
|
64
|
+
process.once("uncaughtException", (err) => {
|
|
65
|
+
logger.error("process.uncaughtException", { error: formatError(err) });
|
|
66
|
+
});
|
|
67
|
+
process.once("unhandledRejection", (reason) => {
|
|
68
|
+
logger.error("process.unhandledRejection", { error: formatError(reason) });
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatError(err) {
|
|
73
|
+
if (err instanceof Error) {
|
|
74
|
+
return {
|
|
75
|
+
name: err.name,
|
|
76
|
+
message: err.message,
|
|
77
|
+
stack: err.stack,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { message: String(err) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sanitize(value, seen = new WeakSet(), key = "") {
|
|
84
|
+
if (SENSITIVE_KEY.test(key)) return REDACTED;
|
|
85
|
+
if (value == null || typeof value === "number" || typeof value === "boolean") return value;
|
|
86
|
+
if (typeof value === "string") return truncateString(value);
|
|
87
|
+
if (typeof value === "bigint") return String(value);
|
|
88
|
+
if (typeof value === "function" || typeof value === "symbol") return `[${typeof value}]`;
|
|
89
|
+
if (value instanceof Error) return sanitize(formatError(value), seen);
|
|
90
|
+
if (typeof value !== "object") return String(value);
|
|
91
|
+
if (seen.has(value)) return "[circular]";
|
|
92
|
+
seen.add(value);
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
const items = value.slice(0, MAX_ARRAY_LENGTH).map((item) => sanitize(item, seen));
|
|
95
|
+
if (value.length > MAX_ARRAY_LENGTH) items.push(`[${value.length - MAX_ARRAY_LENGTH} more items]`);
|
|
96
|
+
return items;
|
|
97
|
+
}
|
|
98
|
+
const entries = Object.entries(value).slice(0, MAX_OBJECT_KEYS);
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [entryKey, entryValue] of entries) out[entryKey] = sanitize(entryValue, seen, entryKey);
|
|
101
|
+
const omitted = Object.keys(value).length - entries.length;
|
|
102
|
+
if (omitted > 0) out.__omittedKeys = omitted;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createChildLogger(parent, extraFields) {
|
|
107
|
+
function withFields(fields) {
|
|
108
|
+
return { ...extraFields, ...(fields ?? {}) };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
enabled: parent.enabled,
|
|
112
|
+
level: parent.level,
|
|
113
|
+
path: parent.path,
|
|
114
|
+
event: (event, fields) => parent.event(event, withFields(fields)),
|
|
115
|
+
debug: (event, fields) => parent.debug(event, withFields(fields)),
|
|
116
|
+
warn: (event, fields) => parent.warn(event, withFields(fields)),
|
|
117
|
+
error: (event, fields) => parent.error(event, withFields(fields)),
|
|
118
|
+
child: (fields = {}) => createChildLogger(parent, withFields(sanitize(fields))),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function defaultLogDir() {
|
|
123
|
+
return join(homedir(), ".march", "logs");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dateStamp(now) {
|
|
127
|
+
return now.toISOString().slice(0, 10);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function truncateString(value) {
|
|
131
|
+
if (value.length <= MAX_STRING_LENGTH) return value;
|
|
132
|
+
return `${value.slice(0, MAX_STRING_LENGTH)}...[truncated ${value.length - MAX_STRING_LENGTH} chars]`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeLevel(level) {
|
|
136
|
+
return LEVELS[String(level).toLowerCase()] ?? LEVELS.info;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function levelName(value) {
|
|
140
|
+
return Object.entries(LEVELS).find(([, level]) => level === value)?.[0] ?? "info";
|
|
141
|
+
}
|
package/src/lsp/client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { spawnCommand } from "../platform/spawn-command.mjs";
|
|
3
3
|
import { extname } from "node:path";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
|
|
@@ -36,7 +36,7 @@ export class LspClient {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async start() {
|
|
39
|
-
this.process =
|
|
39
|
+
this.process = spawnCommand(this.command, this.args, {
|
|
40
40
|
cwd: this.cwd,
|
|
41
41
|
env: process.env,
|
|
42
42
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { lspPathKey } from "./path-match.mjs";
|
|
2
3
|
|
|
3
4
|
export class LspDiagnosticStore {
|
|
4
5
|
constructor() {
|
|
@@ -13,7 +14,7 @@ export class LspDiagnosticStore {
|
|
|
13
14
|
serverId,
|
|
14
15
|
path,
|
|
15
16
|
}));
|
|
16
|
-
this.byPath.set(path, {
|
|
17
|
+
this.byPath.set(lspPathKey(path), {
|
|
17
18
|
path,
|
|
18
19
|
updatedAt: Date.now(),
|
|
19
20
|
diagnostics: normalized,
|
|
@@ -22,10 +23,12 @@ export class LspDiagnosticStore {
|
|
|
22
23
|
|
|
23
24
|
snapshot() {
|
|
24
25
|
const diagnostics = [];
|
|
26
|
+
const files = [];
|
|
25
27
|
for (const entry of this.byPath.values()) {
|
|
26
28
|
diagnostics.push(...entry.diagnostics);
|
|
29
|
+
files.push({ path: entry.path, updatedAt: entry.updatedAt, diagnostics: entry.diagnostics.length });
|
|
27
30
|
}
|
|
28
|
-
return diagnostics;
|
|
31
|
+
return { diagnostics, files };
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sameLspPath } from "./path-match.mjs";
|
|
2
|
+
|
|
1
3
|
const MAX_DIAGNOSTICS = 20;
|
|
2
4
|
|
|
3
5
|
export function formatLspDiagnostics({ snapshot } = {}) {
|
|
@@ -20,7 +22,7 @@ export function formatLspDiagnostics({ snapshot } = {}) {
|
|
|
20
22
|
export function formatLspDiagnosticsForPath({ snapshot, path } = {}) {
|
|
21
23
|
const targetPath = String(path ?? "");
|
|
22
24
|
if (!targetPath) return "";
|
|
23
|
-
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => diagnostic.path
|
|
25
|
+
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => sameLspPath(diagnostic.path, targetPath));
|
|
24
26
|
if (diagnostics.length === 0) return "";
|
|
25
27
|
return formatLspDiagnostics({
|
|
26
28
|
snapshot: {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawnCommand } from "../platform/spawn-command.mjs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CACHE_ROOT = join(homedir(), ".march", "lsp", "node");
|
|
7
|
+
|
|
8
|
+
const MANAGED_PACKAGES = {
|
|
9
|
+
"typescript-language-server": ["typescript-language-server", "typescript"],
|
|
10
|
+
"vue-language-server": ["@vue/language-server", "typescript"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const installs = new Map();
|
|
14
|
+
|
|
15
|
+
export function getManagedNodeLspRoot() {
|
|
16
|
+
return process.env.MARCH_LSP_NODE_ROOT || DEFAULT_CACHE_ROOT;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureManagedNodeCommand(name) {
|
|
20
|
+
if (!MANAGED_PACKAGES[name]) return null;
|
|
21
|
+
const root = getManagedNodeLspRoot();
|
|
22
|
+
const existing = findManagedBin(name, root);
|
|
23
|
+
if (existing) return { command: existing, root, installed: false };
|
|
24
|
+
|
|
25
|
+
await ensureManagedPackages(name, root);
|
|
26
|
+
const command = findManagedBin(name, root);
|
|
27
|
+
if (!command) throw new Error(`managed install did not provide ${name}`);
|
|
28
|
+
return { command, root, installed: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ensureManagedTypeScript() {
|
|
32
|
+
const root = getManagedNodeLspRoot();
|
|
33
|
+
if (findManagedTypeScriptServer()) return { root, installed: false };
|
|
34
|
+
await installPackages(root, ["typescript"]);
|
|
35
|
+
if (!findManagedTypeScriptServer()) throw new Error("managed install did not provide typescript");
|
|
36
|
+
return { root, installed: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findManagedTypeScriptServer() {
|
|
40
|
+
const root = getManagedNodeLspRoot();
|
|
41
|
+
const tsserver = join(root, "node_modules", "typescript", "lib", "tsserver.js");
|
|
42
|
+
return existsSync(tsserver) ? tsserver : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findManagedTypeScriptSdk() {
|
|
46
|
+
const root = getManagedNodeLspRoot();
|
|
47
|
+
const lib = join(root, "node_modules", "typescript", "lib");
|
|
48
|
+
return existsSync(join(lib, "tsserverlibrary.js")) || existsSync(join(lib, "tsserver.js")) ? lib : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function ensureManagedPackages(name, root) {
|
|
52
|
+
const key = `${root}:${name}`;
|
|
53
|
+
if (!installs.has(key)) {
|
|
54
|
+
installs.set(key, installPackages(root, MANAGED_PACKAGES[name]).finally(() => installs.delete(key)));
|
|
55
|
+
}
|
|
56
|
+
return await installs.get(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function installPackages(root, packages) {
|
|
60
|
+
mkdirSync(root, { recursive: true });
|
|
61
|
+
const packageJson = join(root, "package.json");
|
|
62
|
+
if (!existsSync(packageJson)) writeFileSync(packageJson, JSON.stringify({ private: true, name: "march-managed-lsp" }, null, 2));
|
|
63
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
64
|
+
await run(npm, ["install", "--no-audit", "--no-fund", "--save-exact", ...packages], { cwd: root });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findManagedBin(name, root) {
|
|
68
|
+
for (const bin of platformCommandNames(name)) {
|
|
69
|
+
const candidate = join(root, "node_modules", ".bin", bin);
|
|
70
|
+
if (existsSync(candidate)) return candidate;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function platformCommandNames(name) {
|
|
76
|
+
if (process.platform !== "win32") return [name];
|
|
77
|
+
return [`${name}.cmd`, `${name}.exe`, name];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function run(command, args, { cwd }) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const child = spawnCommand(command, args, { cwd, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
83
|
+
let stderr = "";
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
stderr += chunk.toString("utf8");
|
|
86
|
+
if (stderr.length > 4000) stderr = stderr.slice(-4000);
|
|
87
|
+
});
|
|
88
|
+
child.on("error", reject);
|
|
89
|
+
child.on("exit", (code) => {
|
|
90
|
+
if (code === 0) resolve();
|
|
91
|
+
else reject(new Error(`${command} ${args.join(" ")} failed with exit ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { normalize } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sameLspPath(left, right) {
|
|
4
|
+
return lspPathKey(left) === lspPathKey(right);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function lspPathKey(path) {
|
|
8
|
+
const normalized = normalize(String(path ?? ""));
|
|
9
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
10
|
+
}
|
package/src/lsp/servers.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { ensureManagedNodeCommand, ensureManagedTypeScript, findManagedTypeScriptSdk, findManagedTypeScriptServer } from "./managed-node-server.mjs";
|
|
5
|
+
import { resolveTypeScriptProjectRoot } from "./typescript-project-resolver.mjs";
|
|
4
6
|
|
|
5
7
|
const NODE_ROOT_MARKERS = ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package.json"];
|
|
6
8
|
|
|
@@ -10,23 +12,28 @@ const LSP_SERVERS = [
|
|
|
10
12
|
extensions: [".vue"],
|
|
11
13
|
rootMarkers: NODE_ROOT_MARKERS,
|
|
12
14
|
command: ["vue-language-server"],
|
|
15
|
+
managedCommand: "vue-language-server",
|
|
13
16
|
args: ["--stdio"],
|
|
14
17
|
initialization: ({ root, workspaceRoot }) => {
|
|
15
18
|
const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
|
|
16
19
|
return tsdk ? { typescript: { tsdk } } : null;
|
|
17
20
|
},
|
|
21
|
+
managedTypeScript: true,
|
|
18
22
|
missingInitialization: "missing project typescript SDK",
|
|
19
23
|
},
|
|
20
24
|
{
|
|
21
25
|
id: "typescript",
|
|
22
26
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
23
27
|
rootMarkers: NODE_ROOT_MARKERS,
|
|
28
|
+
projectRoot: resolveTypeScriptProjectRoot,
|
|
24
29
|
command: ["typescript-language-server"],
|
|
30
|
+
managedCommand: "typescript-language-server",
|
|
25
31
|
args: ["--stdio"],
|
|
26
32
|
initialization: ({ root, workspaceRoot }) => {
|
|
27
33
|
const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
|
|
28
34
|
return tsserver ? { tsserver: { path: tsserver } } : null;
|
|
29
35
|
},
|
|
36
|
+
managedTypeScript: true,
|
|
30
37
|
missingInitialization: "missing project typescript/tsserver.js",
|
|
31
38
|
},
|
|
32
39
|
{
|
|
@@ -129,23 +136,23 @@ const LSP_SERVERS = [
|
|
|
129
136
|
},
|
|
130
137
|
];
|
|
131
138
|
|
|
132
|
-
export function resolveLspServer({ filePath, workspaceRoot }) {
|
|
133
|
-
const result = resolveLspServerStatus({ filePath, workspaceRoot });
|
|
139
|
+
export async function resolveLspServer({ filePath, workspaceRoot, onEvent = null } = {}) {
|
|
140
|
+
const result = await resolveLspServerStatus({ filePath, workspaceRoot, onEvent });
|
|
134
141
|
return result.status === "available" ? result.server : null;
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
export function resolveLspServerStatus({ filePath, workspaceRoot }) {
|
|
144
|
+
export async function resolveLspServerStatus({ filePath, workspaceRoot, onEvent = null } = {}) {
|
|
138
145
|
const ext = extensionOf(filePath);
|
|
139
146
|
const def = LSP_SERVERS.find((server) => server.extensions.includes(ext));
|
|
140
147
|
if (!def) return { status: "unsupported", extension: ext };
|
|
141
148
|
|
|
142
|
-
const root = def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
+
const root = resolveServerRoot(def, { filePath, workspaceRoot });
|
|
150
|
+
const command = await resolveCommand(def, { root, workspaceRoot, onEvent });
|
|
151
|
+
if (command?.error) return { status: "unavailable", id: def.id, root, reason: command.error };
|
|
152
|
+
if (!command) return { status: "unavailable", id: def.id, root, reason: `missing ${def.command[0]}` };
|
|
153
|
+
|
|
154
|
+
const managedTypeScript = await ensureTypeScriptFallback(def, { root, workspaceRoot, onEvent });
|
|
155
|
+
if (managedTypeScript?.error) return { status: "unavailable", id: def.id, root, reason: managedTypeScript.error };
|
|
149
156
|
|
|
150
157
|
const initialization = def.initialization?.({ root, workspaceRoot }) ?? {};
|
|
151
158
|
if (initialization === null) {
|
|
@@ -153,7 +160,7 @@ export function resolveLspServerStatus({ filePath, workspaceRoot }) {
|
|
|
153
160
|
}
|
|
154
161
|
return {
|
|
155
162
|
status: "available",
|
|
156
|
-
server: { id: def.id, command, args: def.args, root, initialization },
|
|
163
|
+
server: { id: def.id, command: command.command, args: def.args, root, initialization, managed: command.managed },
|
|
157
164
|
};
|
|
158
165
|
}
|
|
159
166
|
|
|
@@ -161,6 +168,42 @@ export function listLspServerDefinitions() {
|
|
|
161
168
|
return LSP_SERVERS.map(({ id, extensions, rootMarkers, command, args }) => ({ id, extensions, rootMarkers, command, args }));
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
async function resolveCommand(def, { root, workspaceRoot, onEvent }) {
|
|
172
|
+
const local = findCommand(def.command, { root, workspaceRoot });
|
|
173
|
+
if (local) return { command: local, managed: false };
|
|
174
|
+
if (!def.managedCommand) return null;
|
|
175
|
+
onEvent?.({ status: "installing", id: def.id, root, reason: `installing ${def.managedCommand}` });
|
|
176
|
+
try {
|
|
177
|
+
const managed = await ensureManagedNodeCommand(def.managedCommand);
|
|
178
|
+
return { command: managed.command, managed: true };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const reason = err.message;
|
|
181
|
+
onEvent?.({ status: "failed", id: def.id, root, reason });
|
|
182
|
+
return { error: reason };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function ensureTypeScriptFallback(def, { root, workspaceRoot, onEvent }) {
|
|
187
|
+
if (!def.managedTypeScript || resolveTypeScriptServer({ root, workspaceRoot })) return null;
|
|
188
|
+
onEvent?.({ status: "installing", id: def.id, root, reason: "installing typescript" });
|
|
189
|
+
try {
|
|
190
|
+
await ensureManagedTypeScript();
|
|
191
|
+
return null;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const reason = err.message;
|
|
194
|
+
onEvent?.({ status: "failed", id: def.id, root, reason });
|
|
195
|
+
return { error: reason };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveServerRoot(def, { filePath, workspaceRoot }) {
|
|
200
|
+
const projectRoot = def.projectRoot?.({ filePath, workspaceRoot });
|
|
201
|
+
if (projectRoot) return projectRoot;
|
|
202
|
+
return def.rootMarkers.length > 0
|
|
203
|
+
? findNearestRoot(dirname(filePath), workspaceRoot, def.rootMarkers) ?? workspaceRoot
|
|
204
|
+
: workspaceRoot;
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
function findCommand(names, { root, workspaceRoot }) {
|
|
165
208
|
for (const name of names) {
|
|
166
209
|
const hit = findBin(name, { root, workspaceRoot });
|
|
@@ -197,13 +240,13 @@ function findOnPath(names) {
|
|
|
197
240
|
}
|
|
198
241
|
|
|
199
242
|
function resolveTypeScriptServer({ root, workspaceRoot }) {
|
|
200
|
-
return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot });
|
|
243
|
+
return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot }) ?? findManagedTypeScriptServer();
|
|
201
244
|
}
|
|
202
245
|
|
|
203
246
|
function resolveTypeScriptSdk({ root, workspaceRoot }) {
|
|
204
247
|
const serverLibrary = resolveModuleFromRoots("typescript/lib/tsserverlibrary.js", { root, workspaceRoot });
|
|
205
248
|
const tsserver = serverLibrary ?? resolveTypeScriptServer({ root, workspaceRoot });
|
|
206
|
-
return tsserver ? dirname(tsserver) :
|
|
249
|
+
return tsserver ? dirname(tsserver) : findManagedTypeScriptSdk();
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
function resolveModuleFromRoots(id, { root, workspaceRoot }) {
|
package/src/lsp/service.mjs
CHANGED
|
@@ -13,8 +13,8 @@ export class LspService {
|
|
|
13
13
|
this.announced = new Set();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
touchFile(path) {
|
|
17
|
-
const result = resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd });
|
|
16
|
+
async touchFile(path) {
|
|
17
|
+
const result = await resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd, onEvent: (event) => this.onEvent?.(event) });
|
|
18
18
|
if (result.status === "unsupported") return result;
|
|
19
19
|
if (result.status === "unavailable") {
|
|
20
20
|
this.unavailable.set(result.id, result);
|
|
@@ -34,7 +34,7 @@ export class LspService {
|
|
|
34
34
|
return { status: "starting", id: server.id, root: server.root };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root });
|
|
37
|
+
this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root, managed: server.managed });
|
|
38
38
|
const task = this.#startClient(server, key).then((client) => {
|
|
39
39
|
client?.touchFile(path);
|
|
40
40
|
return client;
|
|
@@ -47,7 +47,7 @@ export class LspService {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
snapshot() {
|
|
50
|
-
const
|
|
50
|
+
const storeSnapshot = this.store.snapshot();
|
|
51
51
|
const servers = [
|
|
52
52
|
...[...this.clients.values()].map((client) => ({
|
|
53
53
|
id: client.serverId,
|
|
@@ -61,7 +61,7 @@ export class LspService {
|
|
|
61
61
|
})),
|
|
62
62
|
...this.unavailable.values(),
|
|
63
63
|
];
|
|
64
|
-
return { status: summarizeStatus(servers), diagnostics, servers };
|
|
64
|
+
return { status: summarizeStatus(servers), diagnostics: storeSnapshot.diagnostics, files: storeSnapshot.files, servers };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
async dispose() {
|
|
@@ -81,7 +81,7 @@ export class LspService {
|
|
|
81
81
|
await client.start();
|
|
82
82
|
this.clients.set(key, client);
|
|
83
83
|
this.unavailable.delete(server.id);
|
|
84
|
-
this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root });
|
|
84
|
+
this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root, managed: server.managed });
|
|
85
85
|
return client;
|
|
86
86
|
} catch (err) {
|
|
87
87
|
client.status = "failed";
|
|
@@ -2,6 +2,7 @@ export function formatLspServiceEvent(event) {
|
|
|
2
2
|
const id = event?.id ? String(event.id) : "server";
|
|
3
3
|
if (event?.status === "attached") return `LSP attached: ${id}`;
|
|
4
4
|
if (event?.status === "starting") return `LSP starting: ${id}`;
|
|
5
|
+
if (event?.status === "installing") return `LSP installing: ${id} - ${event.reason}`;
|
|
5
6
|
if (event?.status === "failed") return `LSP failed: ${id} - ${event.reason}`;
|
|
6
7
|
if (event?.status === "unavailable") return `LSP unavailable: ${id} - ${event.reason}`;
|
|
7
8
|
return `LSP ${event?.status ?? "status"}: ${id}`;
|