pi-lsp-lite 0.3.2 → 0.4.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/index.ts CHANGED
@@ -4,9 +4,10 @@ import { languageForFile, checkExtensionOverlaps, builtinLanguages, type Languag
4
4
  import { formatDiagnostics } from "./src/format.js";
5
5
  import { DiagnosticSeverity } from "vscode-languageserver-protocol";
6
6
  import { loadConfig, writeGlobalConfig, readGlobalConfig } from "./src/config.js";
7
- import { fileUri, which } from "./src/util.js";
7
+ import { fileUri, which, isInsideCwd } from "./src/util.js";
8
8
  import { installRegistry } from "./src/install-registry.js";
9
- import { resolve, relative, isAbsolute } from "node:path";
9
+ import { resolve } from "node:path";
10
+ import { realpath } from "node:fs/promises";
10
11
  import { fileURLToPath } from "node:url";
11
12
 
12
13
  export default function (pi: ExtensionAPI) {
@@ -38,16 +39,21 @@ export default function (pi: ExtensionAPI) {
38
39
  const rawPath = event.input?.path;
39
40
  const filePath = typeof rawPath === "string" ? rawPath : undefined;
40
41
  if (!filePath) return;
42
+ if (event.isError) return;
41
43
 
42
- const absolutePath = resolve(ctx.cwd, filePath);
43
- const rel = relative(ctx.cwd, absolutePath);
44
- if (!rel || rel.startsWith("..") || isAbsolute(rel)) return;
44
+ let absolutePath: string;
45
+ try {
46
+ absolutePath = await realpath(resolve(ctx.cwd, filePath));
47
+ } catch {
48
+ return;
49
+ }
50
+ if (!isInsideCwd(absolutePath, ctx.cwd)) return;
45
51
  const langConfig = languageForFile(absolutePath, servers);
46
52
  if (!langConfig) return;
47
53
 
48
54
  try {
49
55
  const result = await manager.handleEdit(absolutePath, langConfig, ctx.cwd);
50
- const formatted = formatDiagnostics(filePath, result);
56
+ const formatted = formatDiagnostics(filePath, result, ctx.cwd);
51
57
  if (!formatted) return;
52
58
 
53
59
  ctx.ui.notify(formatted.trim(), "warning");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lsp-lite",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "LSP diagnostics for pi — errors and warnings on every edit, same turn. Go, Rust, TypeScript, Python, C/C++.",
5
5
  "type": "module",
6
6
  "private": false,
package/src/client.ts CHANGED
@@ -21,6 +21,7 @@ export interface OtherFileDiagnostics {
21
21
  uri: string;
22
22
  errorCount: number;
23
23
  warningCount: number;
24
+ firstDiagnostic?: { severity: number; line: number; col: number; message: string; source?: string };
24
25
  }
25
26
 
26
27
  export interface DiagnosticResult {
@@ -53,6 +54,28 @@ function countDiagnostics(diags: Diagnostic[]): { errors: number; warnings: numb
53
54
  return { errors, warnings };
54
55
  }
55
56
 
57
+ function diagnosticFingerprint(d: Diagnostic): string {
58
+ return `${d.severity}:${d.range.start.line}:${d.range.start.character}:${d.message}`;
59
+ }
60
+
61
+ function fingerprintSet(diags: Diagnostic[]): Set<string> {
62
+ const set = new Set<string>();
63
+ for (const d of diags) {
64
+ if (d.severity === DiagnosticSeverity.Error || d.severity === DiagnosticSeverity.Warning) {
65
+ set.add(diagnosticFingerprint(d));
66
+ }
67
+ }
68
+ return set;
69
+ }
70
+
71
+ function setsEqual(a: Set<string>, b: Set<string>): boolean {
72
+ if (a.size !== b.size) return false;
73
+ for (const v of a) {
74
+ if (!b.has(v)) return false;
75
+ }
76
+ return true;
77
+ }
78
+
56
79
  export function createLspClient(child: ChildProcess): LspClient {
57
80
  if (!child.stdout || !child.stdin) {
58
81
  throw new Error("LSP child process must be spawned with stdio: pipe");
@@ -152,10 +175,10 @@ export function createLspClient(child: ChildProcess): LspClient {
152
175
  async waitForDiagnostics(uri: string, timeoutMs: number): Promise<DiagnosticResult> {
153
176
  const targetGen = uriGeneration.get(uri) ?? 0;
154
177
 
155
- const preSnapshot = new Map<string, { errors: number; warnings: number }>();
178
+ const preSnapshot = new Map<string, Set<string>>();
156
179
  for (const [trackedUri, entry] of diagnosticsMap) {
157
180
  if (trackedUri !== uri) {
158
- preSnapshot.set(trackedUri, countDiagnostics(entry.diagnostics));
181
+ preSnapshot.set(trackedUri, fingerprintSet(entry.diagnostics));
159
182
  }
160
183
  }
161
184
 
@@ -163,13 +186,27 @@ export function createLspClient(child: ChildProcess): LspClient {
163
186
  const result: OtherFileDiagnostics[] = [];
164
187
  for (const [trackedUri, entry] of diagnosticsMap) {
165
188
  if (trackedUri === uri) continue;
189
+ const postFp = fingerprintSet(entry.diagnostics);
190
+ const preFp = preSnapshot.get(trackedUri) ?? new Set();
191
+ if (setsEqual(postFp, preFp)) continue;
166
192
  const post = countDiagnostics(entry.diagnostics);
167
- const pre = preSnapshot.get(trackedUri) ?? { errors: 0, warnings: 0 };
168
- const newErrors = post.errors - pre.errors;
169
- const newWarnings = post.warnings - pre.warnings;
170
- if (newErrors > 0 || newWarnings > 0) {
171
- result.push({ uri: trackedUri, errorCount: newErrors, warningCount: newWarnings });
172
- }
193
+ const first =
194
+ entry.diagnostics.find((d) => d.severity === DiagnosticSeverity.Error) ??
195
+ entry.diagnostics.find((d) => d.severity === DiagnosticSeverity.Warning);
196
+ result.push({
197
+ uri: trackedUri,
198
+ errorCount: post.errors,
199
+ warningCount: post.warnings,
200
+ ...(first && {
201
+ firstDiagnostic: {
202
+ severity: first.severity ?? DiagnosticSeverity.Error,
203
+ line: first.range.start.line,
204
+ col: first.range.start.character,
205
+ message: first.message,
206
+ ...(first.source && { source: first.source }),
207
+ },
208
+ }),
209
+ });
173
210
  }
174
211
  return result;
175
212
  };
@@ -214,9 +251,9 @@ export function createLspClient(child: ChildProcess): LspClient {
214
251
  // edited file is valid but dependents break
215
252
  crossFileCallback = (changedUri: string) => {
216
253
  if (settled || changedUri === uri) return;
217
- const pre = preSnapshot.get(changedUri) ?? { errors: 0, warnings: 0 };
218
- const post = countDiagnostics(diagnosticsMap.get(changedUri)?.diagnostics ?? []);
219
- if (post.errors !== pre.errors || post.warnings !== pre.warnings) {
254
+ const preFp = preSnapshot.get(changedUri) ?? new Set<string>();
255
+ const postFp = fingerprintSet(diagnosticsMap.get(changedUri)?.diagnostics ?? []);
256
+ if (!setsEqual(preFp, postFp)) {
220
257
  clearTimeout(timeout);
221
258
  resetQuiescence();
222
259
  }
package/src/config.ts CHANGED
@@ -66,7 +66,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
66
66
  console.error(`[pi-lsp-lite] config "${id}": extensions must be a non-empty string array, skipping`);
67
67
  return null;
68
68
  }
69
- override.extensions = (raw.extensions as string[]).map((e) => e.toLowerCase());
69
+ override.extensions = raw.extensions.map((e) => e.toLowerCase());
70
70
  }
71
71
 
72
72
  if (raw.command !== undefined) {
@@ -74,7 +74,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
74
74
  console.error(`[pi-lsp-lite] config "${id}": command must be a non-empty string, skipping`);
75
75
  return null;
76
76
  }
77
- override.command = raw.command as string;
77
+ override.command = raw.command;
78
78
  }
79
79
 
80
80
  if (raw.args !== undefined) {
@@ -82,7 +82,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
82
82
  console.error(`[pi-lsp-lite] config "${id}": args must be a string array, skipping`);
83
83
  return null;
84
84
  }
85
- override.args = raw.args as string[];
85
+ override.args = raw.args;
86
86
  }
87
87
 
88
88
  if (raw.rootPatterns !== undefined) {
@@ -90,7 +90,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
90
90
  console.error(`[pi-lsp-lite] config "${id}": rootPatterns must be a string array, skipping`);
91
91
  return null;
92
92
  }
93
- override.rootPatterns = raw.rootPatterns as string[];
93
+ override.rootPatterns = raw.rootPatterns;
94
94
  }
95
95
 
96
96
  if (raw.diagnosticTimeout !== undefined) {
@@ -150,12 +150,18 @@ async function findProjectConfig(cwd: string): Promise<UserConfig | null> {
150
150
 
151
151
  type ConfigSource = "global" | "project";
152
152
 
153
+ interface MergeResult {
154
+ servers: LanguageServerConfig[];
155
+ perServerTimeouts: Map<string, number>;
156
+ }
157
+
153
158
  function mergeConfigs(
154
159
  base: LanguageServerConfig[],
155
160
  overrides: Record<string, ServerConfigOverride>,
156
161
  source: ConfigSource,
157
- ): LanguageServerConfig[] {
162
+ ): MergeResult {
158
163
  const result = new Map<string, LanguageServerConfig>();
164
+ const perServerTimeouts = new Map<string, number>();
159
165
 
160
166
  for (const server of base) {
161
167
  result.set(server.id, { ...server });
@@ -170,9 +176,17 @@ function mergeConfigs(
170
176
  continue;
171
177
  }
172
178
 
179
+ if (override.diagnosticTimeout !== undefined) {
180
+ perServerTimeouts.set(id, override.diagnosticTimeout);
181
+ }
182
+
173
183
  const existing = result.get(id);
174
184
  if (existing) {
175
185
  const { disabled: _, diagnosticTimeout: __, ...lspFields } = override;
186
+ if (source === "project" && lspFields.command !== undefined) {
187
+ console.error(`[pi-lsp-lite] project config cannot override "command" for server "${id}" — ignoring (use global config instead)`);
188
+ delete lspFields.command;
189
+ }
176
190
  const defined = Object.fromEntries(
177
191
  Object.entries(lspFields).filter(([, v]) => v !== undefined),
178
192
  );
@@ -197,7 +211,7 @@ function mergeConfigs(
197
211
  }
198
212
  }
199
213
 
200
- return Array.from(result.values());
214
+ return { servers: Array.from(result.values()), perServerTimeouts };
201
215
  }
202
216
 
203
217
  export function globalConfigFilePath(globalConfigPath?: string): string {
@@ -275,12 +289,10 @@ export async function loadConfig(cwd: string, globalConfigPath?: string): Promis
275
289
  for (const [layer, source] of layers) {
276
290
  if (!layer) continue;
277
291
  if (layer.servers && isPlainObject(layer.servers)) {
278
- servers = mergeConfigs(servers, layer.servers as Record<string, ServerConfigOverride>, source);
279
- for (const [id, rawOverride] of Object.entries(layer.servers)) {
280
- const override = validateOverride(id, rawOverride);
281
- if (override?.diagnosticTimeout !== undefined) {
282
- perServerTimeout.set(id, override.diagnosticTimeout);
283
- }
292
+ const merged = mergeConfigs(servers, layer.servers as Record<string, ServerConfigOverride>, source);
293
+ servers = merged.servers;
294
+ for (const [id, timeout] of merged.perServerTimeouts) {
295
+ perServerTimeout.set(id, timeout);
284
296
  }
285
297
  }
286
298
  if (layer.diagnosticTimeout !== undefined) {
package/src/format.ts CHANGED
@@ -1,20 +1,30 @@
1
1
  import { DiagnosticSeverity } from "vscode-languageserver-protocol";
2
+ import { fileURLToPath } from "node:url";
3
+ import { relative } from "node:path";
2
4
  import type { DiagnosticResult } from "./client.js";
3
5
 
4
- export function formatDiagnostics(filePath: string, result: DiagnosticResult): string {
5
- const relevant = result.diagnostics.filter(
6
+ const MAX_DIAGNOSTICS_PER_FILE = 50;
7
+
8
+ export function formatDiagnostics(filePath: string, result: DiagnosticResult, cwd?: string): string {
9
+ const allRelevant = result.diagnostics.filter(
6
10
  (d) => d.severity === DiagnosticSeverity.Error || d.severity === DiagnosticSeverity.Warning,
7
11
  );
8
12
 
9
- if (relevant.length === 0 && result.status === "ok" && result.otherFiles.length === 0) return "";
10
- if (result.status === "unavailable") return "";
13
+ if (allRelevant.length === 0 && result.status === "ok" && result.otherFiles.length === 0) return "";
14
+
15
+ if (result.status === "unavailable") {
16
+ return `\n⚠ LSP diagnostics unavailable for ${filePath} (server missing or failed to start)`;
17
+ }
18
+
19
+ const truncated = allRelevant.length > MAX_DIAGNOSTICS_PER_FILE;
20
+ const relevant = truncated ? allRelevant.slice(0, MAX_DIAGNOSTICS_PER_FILE) : allRelevant;
11
21
 
12
22
  const retryNote = result.status === "timeout" && result.retryAttempts > 0
13
23
  ? ` after ${result.retryAttempts} ${result.retryAttempts === 1 ? "retry" : "retries"}`
14
24
  : "";
15
25
 
16
26
  if (relevant.length === 0 && result.status === "ok" && result.otherFiles.length > 0) {
17
- return `\n⚠ LSP diagnostics for ${filePath}: no issues${otherFilesFooter(result)}`;
27
+ return `\n⚠ LSP diagnostics for ${filePath}: no issues${otherFilesFooter(result, cwd)}`;
18
28
  }
19
29
 
20
30
  const lines = relevant.map((d) => {
@@ -25,8 +35,11 @@ export function formatDiagnostics(filePath: string, result: DiagnosticResult): s
25
35
  return ` ${severity} ${line}:${col} ${source}${d.message}`;
26
36
  });
27
37
 
28
- const errorCount = relevant.filter((d) => d.severity === DiagnosticSeverity.Error).length;
29
- const warnCount = relevant.length - errorCount;
38
+ let errorCount = 0;
39
+ for (const d of allRelevant) {
40
+ if (d.severity === DiagnosticSeverity.Error) errorCount++;
41
+ }
42
+ const warnCount = allRelevant.length - errorCount;
30
43
 
31
44
  const summary = [
32
45
  errorCount > 0 ? `${errorCount} error${errorCount > 1 ? "s" : ""}` : "",
@@ -36,12 +49,30 @@ export function formatDiagnostics(filePath: string, result: DiagnosticResult): s
36
49
  .filter(Boolean)
37
50
  .join(", ");
38
51
 
39
- return `\n LSP diagnostics for ${filePath} (${summary}):\n${lines.join("\n")}${otherFilesFooter(result)}`;
52
+ const truncatedNote = truncated ? `\n ... and ${allRelevant.length - MAX_DIAGNOSTICS_PER_FILE} more` : "";
53
+
54
+ return `\n⚠ LSP diagnostics for ${filePath} (${summary}):\n${lines.join("\n")}${truncatedNote}${otherFilesFooter(result, cwd)}`;
40
55
  }
41
56
 
42
- function otherFilesFooter(result: DiagnosticResult): string {
57
+ function otherFilesFooter(result: DiagnosticResult, cwd?: string): string {
43
58
  if (result.otherFiles.length === 0) return "";
44
- const totalDiags = result.otherFiles.reduce((sum, f) => sum + f.errorCount + f.warningCount, 0);
45
- const fileCount = result.otherFiles.length;
46
- return `\n + ${totalDiags} diagnostic${totalDiags !== 1 ? "s" : ""} in ${fileCount} other file${fileCount !== 1 ? "s" : ""}`;
59
+ const lines = result.otherFiles.map((f) => {
60
+ let path: string;
61
+ try {
62
+ const abs = fileURLToPath(f.uri);
63
+ path = cwd ? relative(cwd, abs) : abs;
64
+ } catch {
65
+ path = f.uri;
66
+ }
67
+ const counts = [
68
+ f.errorCount > 0 ? `${f.errorCount} error${f.errorCount > 1 ? "s" : ""}` : "",
69
+ f.warningCount > 0 ? `${f.warningCount} warning${f.warningCount > 1 ? "s" : ""}` : "",
70
+ ].filter(Boolean).join(", ");
71
+ if (!f.firstDiagnostic) return ` ${path} (${counts})`;
72
+ const d = f.firstDiagnostic;
73
+ const sev = d.severity === DiagnosticSeverity.Error ? "error" : "warning";
74
+ const src = d.source ? `[${d.source}] ` : "";
75
+ return ` ${path} (${counts}): ${sev} ${d.line + 1}:${d.col + 1} ${src}${d.message}`;
76
+ });
77
+ return `\n${lines.join("\n")}`;
47
78
  }
package/src/languages.ts CHANGED
@@ -6,6 +6,7 @@ export interface LanguageServerConfig {
6
6
  rootPatterns: string[];
7
7
  diagnosticTimeout?: number;
8
8
  maxRetries?: number;
9
+ languageIds?: Record<string, string>;
9
10
  }
10
11
 
11
12
  export const builtinLanguages: LanguageServerConfig[] = [
@@ -32,6 +33,7 @@ export const builtinLanguages: LanguageServerConfig[] = [
32
33
  args: ["--stdio"],
33
34
  rootPatterns: ["tsconfig.json", "package.json"],
34
35
  diagnosticTimeout: 30_000,
36
+ languageIds: { ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" },
35
37
  },
36
38
  {
37
39
  id: "python",
@@ -48,6 +50,7 @@ export const builtinLanguages: LanguageServerConfig[] = [
48
50
  args: [],
49
51
  rootPatterns: ["compile_commands.json", "CMakeLists.txt", ".clangd"],
50
52
  diagnosticTimeout: 15_000,
53
+ languageIds: { ".c": "c", ".h": "c", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp", ".hxx": "cpp" },
51
54
  },
52
55
  ];
53
56
 
@@ -56,6 +59,14 @@ export function languageForFile(path: string, configs: LanguageServerConfig[]):
56
59
  return configs.find((lang) => lang.extensions.some((ext) => lower.endsWith(ext)));
57
60
  }
58
61
 
62
+ export function languageIdForFile(filePath: string, config: LanguageServerConfig): string {
63
+ if (config.languageIds) {
64
+ const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0];
65
+ if (ext && config.languageIds[ext]) return config.languageIds[ext];
66
+ }
67
+ return config.id;
68
+ }
69
+
59
70
  export function checkExtensionOverlaps(configs: LanguageServerConfig[]): string[] {
60
71
  const warnings: string[] = [];
61
72
  const seen = new Map<string, string>();
@@ -1,7 +1,7 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { which, fileUri, findWorkspaceRoot } from "./util.js";
3
3
  import { createLspClient, type LspClient, type DiagnosticResult } from "./client.js";
4
- import type { LanguageServerConfig } from "./languages.js";
4
+ import { type LanguageServerConfig, languageIdForFile } from "./languages.js";
5
5
  import type { Diagnostic } from "vscode-languageserver-protocol";
6
6
  import { DEFAULT_DIAGNOSTIC_TIMEOUT, DEFAULT_DOCUMENT_IDLE_TIMEOUT, DEFAULT_MAX_RETRIES } from "./config.js";
7
7
  import { readFile } from "node:fs/promises";
@@ -221,7 +221,7 @@ export function createServerManager(options: ServerManagerOptions = {}): ServerM
221
221
  if (server.openDocuments.has(uri)) {
222
222
  server.client.didChange(uri, content);
223
223
  } else {
224
- server.client.didOpen(uri, server.config.id, content);
224
+ server.client.didOpen(uri, languageIdForFile(filePath, server.config), content);
225
225
  }
226
226
  server.openDocuments.set(uri, Date.now());
227
227
 
package/src/util.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { access, constants } from "node:fs/promises";
2
- import { join, dirname } from "node:path";
2
+ import { join, dirname, relative, isAbsolute } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
 
5
5
  export function fileUri(absolutePath: string): string {
@@ -42,3 +42,8 @@ export async function findWorkspaceRoot(filePath: string, rootPatterns: string[]
42
42
  }
43
43
  return cwd;
44
44
  }
45
+
46
+ export function isInsideCwd(absolutePath: string, cwd: string): boolean {
47
+ const rel = relative(cwd, absolutePath);
48
+ return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
49
+ }
@@ -1,30 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- check:
14
- name: typecheck + unit tests
15
- runs-on: ubuntu-latest
16
- steps:
17
- - uses: actions/checkout@v6
18
-
19
- - uses: actions/setup-node@v6
20
- with:
21
- node-version: 20
22
- cache: npm
23
-
24
- - run: npm ci
25
-
26
- - name: typecheck
27
- run: npm run check
28
-
29
- - name: unit tests
30
- run: npm test
@@ -1,79 +0,0 @@
1
- name: Integration Tests
2
-
3
- on:
4
- pull_request:
5
- branches: [main]
6
-
7
- permissions:
8
- contents: read
9
-
10
- jobs:
11
- gopls:
12
- name: gopls
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v6
16
- - uses: actions/setup-node@v6
17
- with:
18
- node-version: 20
19
- cache: npm
20
- - uses: actions/setup-go@v6
21
- with:
22
- go-version: stable
23
- - run: go install golang.org/x/tools/gopls@latest
24
- - run: npm ci
25
- - run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/gopls.test.ts
26
-
27
- rust-analyzer:
28
- name: rust-analyzer
29
- runs-on: ubuntu-latest
30
- steps:
31
- - uses: actions/checkout@v6
32
- - uses: actions/setup-node@v6
33
- with:
34
- node-version: 20
35
- cache: npm
36
- - run: |
37
- rustup update stable
38
- rustup component add rust-analyzer
39
- - run: npm ci
40
- - run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/rust-analyzer.test.ts
41
-
42
- typescript:
43
- name: typescript-language-server
44
- runs-on: ubuntu-latest
45
- steps:
46
- - uses: actions/checkout@v6
47
- - uses: actions/setup-node@v6
48
- with:
49
- node-version: 20
50
- cache: npm
51
- - run: npm install -g typescript-language-server typescript
52
- - run: npm ci
53
- - run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/typescript.test.ts
54
-
55
- pylsp:
56
- name: pylsp
57
- runs-on: ubuntu-latest
58
- steps:
59
- - uses: actions/checkout@v6
60
- - uses: actions/setup-node@v6
61
- with:
62
- node-version: 20
63
- cache: npm
64
- - run: pip install 'python-lsp-server[all]'
65
- - run: npm ci
66
- - run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/pylsp.test.ts
67
-
68
- clangd:
69
- name: clangd
70
- runs-on: ubuntu-latest
71
- steps:
72
- - uses: actions/checkout@v6
73
- - uses: actions/setup-node@v6
74
- with:
75
- node-version: 20
76
- cache: npm
77
- - run: sudo apt-get update -qq && sudo apt-get install -y -qq clangd
78
- - run: npm ci
79
- - run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/clangd.test.ts
@@ -1,34 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- release:
5
- types: [published]
6
-
7
- permissions:
8
- contents: read
9
- id-token: write
10
-
11
- jobs:
12
- publish:
13
- name: publish to npm
14
- runs-on: ubuntu-latest
15
- environment: public
16
- steps:
17
- - uses: actions/checkout@v6
18
-
19
- - uses: actions/setup-node@v6
20
- with:
21
- node-version: 24
22
- registry-url: https://registry.npmjs.org
23
- cache: npm
24
-
25
- - run: npm ci
26
-
27
- - name: typecheck
28
- run: npm run check
29
-
30
- - name: unit tests
31
- run: npm test
32
-
33
- - name: publish
34
- run: npm publish --access public