pi-x-ide 1.4.0 → 1.4.2

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 (43) hide show
  1. package/README.md +183 -114
  2. package/README.zh.md +183 -114
  3. package/dist/package.json +10 -4
  4. package/dist/src/nvim/sidecar-schema.d.ts +25 -0
  5. package/dist/src/nvim/sidecar-schema.js +64 -0
  6. package/dist/src/nvim/sidecar-schema.js.map +1 -0
  7. package/dist/src/nvim/sidecar.d.ts +16 -0
  8. package/dist/src/nvim/sidecar.js +173 -0
  9. package/dist/src/nvim/sidecar.js.map +1 -0
  10. package/dist/src/pi/index.js +10 -2
  11. package/dist/src/pi/index.js.map +1 -1
  12. package/dist/src/pi/install.js +22 -1
  13. package/dist/src/pi/install.js.map +1 -1
  14. package/dist/src/shared/ide-server.d.ts +20 -0
  15. package/dist/src/shared/ide-server.js +144 -0
  16. package/dist/src/shared/ide-server.js.map +1 -0
  17. package/dist/src/shared/lock-file.d.ts +16 -0
  18. package/dist/src/shared/lock-file.js +58 -0
  19. package/dist/src/shared/lock-file.js.map +1 -0
  20. package/dist/src/shared/paths.js +8 -1
  21. package/dist/src/shared/paths.js.map +1 -1
  22. package/dist/src/shared/protocol.d.ts +1 -1
  23. package/dist/src/shared/schema.js +1 -1
  24. package/dist/src/shared/schema.js.map +1 -1
  25. package/dist/test/nvim-sidecar.test.d.ts +1 -0
  26. package/dist/test/nvim-sidecar.test.js +148 -0
  27. package/dist/test/nvim-sidecar.test.js.map +1 -0
  28. package/dist/test/shared.test.js +10 -0
  29. package/dist/test/shared.test.js.map +1 -1
  30. package/nvim/bin/pi-x-ide-nvim-sidecar.cjs +21 -0
  31. package/nvim/doc/pi-x-ide.txt +112 -0
  32. package/nvim/lua/pi_x_ide/init.lua +442 -0
  33. package/nvim/plugin/pi-x-ide.lua +15 -0
  34. package/package.json +10 -4
  35. package/src/nvim/sidecar-schema.ts +71 -0
  36. package/src/nvim/sidecar.ts +219 -0
  37. package/src/pi/index.ts +12 -2
  38. package/src/pi/install.ts +24 -1
  39. package/src/shared/ide-server.ts +120 -0
  40. package/src/shared/lock-file.ts +65 -0
  41. package/src/shared/paths.ts +8 -1
  42. package/src/shared/protocol.ts +1 -1
  43. package/src/shared/schema.ts +1 -1
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import { LOCK_DIR_ENV, type EditorSelectionSnapshot, type IdeLockFile } from "../shared/protocol";
4
+ import { formatRangeMention } from "../shared/format";
5
+ import { IdeWebSocketServer } from "../shared/ide-server";
6
+ import {
7
+ createAuthToken,
8
+ createIdeLockFile,
9
+ createIdeLockFilePath,
10
+ refreshIdeLockFile,
11
+ removeIdeLockFile,
12
+ writeIdeLockFile,
13
+ } from "../shared/lock-file";
14
+ import { parseJsonLine, parseNvimSidecarMessage, parseSidecarConfig, type NvimSidecarMessage } from "./sidecar-schema";
15
+
16
+ export interface NvimSidecarOptions {
17
+ workspaceFolders?: string[];
18
+ name?: string;
19
+ lockDir?: string;
20
+ stdin?: NodeJS.ReadableStream;
21
+ stdout?: NodeJS.WritableStream;
22
+ stderr?: NodeJS.WritableStream;
23
+ }
24
+
25
+ export interface NvimSidecarHandle {
26
+ server: IdeWebSocketServer;
27
+ lockFilePath: string;
28
+ stop: () => Promise<void>;
29
+ }
30
+
31
+ interface RuntimeState {
32
+ latestSelection?: EditorSelectionSnapshot;
33
+ workspaceFolders: string[];
34
+ lockFile?: IdeLockFile;
35
+ lockFilePath?: string;
36
+ stopped: boolean;
37
+ }
38
+
39
+ export async function startNvimSidecar(options: NvimSidecarOptions = {}): Promise<NvimSidecarHandle> {
40
+ const previousLockDir = process.env[LOCK_DIR_ENV];
41
+ if (options.lockDir) process.env[LOCK_DIR_ENV] = options.lockDir;
42
+
43
+ const state: RuntimeState = {
44
+ workspaceFolders: normalizeWorkspaceFolders(options.workspaceFolders),
45
+ stopped: false,
46
+ };
47
+
48
+ const authToken = createAuthToken();
49
+ const server = new IdeWebSocketServer(
50
+ authToken,
51
+ { name: options.name ?? "Neovim", ide: "nvim" },
52
+ () => state.latestSelection,
53
+ );
54
+
55
+ const port = await server.start();
56
+ state.lockFilePath = createIdeLockFilePath("nvim", port);
57
+ state.lockFile = createIdeLockFile({
58
+ ide: "nvim",
59
+ name: options.name ?? "Neovim",
60
+ port,
61
+ authToken,
62
+ workspaceFolders: state.workspaceFolders,
63
+ });
64
+ await writeIdeLockFile(state.lockFilePath, state.lockFile);
65
+
66
+ const stdout = options.stdout ?? process.stdout;
67
+ stdout.write(JSON.stringify({ type: "ready", port, lockFilePath: state.lockFilePath }) + "\n");
68
+
69
+ const stop = async () => {
70
+ if (state.stopped) return;
71
+ state.stopped = true;
72
+ await removeIdeLockFile(state.lockFilePath);
73
+ await server.stop();
74
+ if (options.lockDir) {
75
+ if (previousLockDir === undefined) delete process.env[LOCK_DIR_ENV];
76
+ else process.env[LOCK_DIR_ENV] = previousLockDir;
77
+ }
78
+ };
79
+
80
+ const stdin = options.stdin ?? process.stdin;
81
+ const stderr = options.stderr ?? process.stderr;
82
+ const rl = createInterface({ input: stdin });
83
+ rl.on("line", (line) => {
84
+ const trimmed = line.trim();
85
+ if (!trimmed) return;
86
+ const parsed = parseJsonLine(trimmed);
87
+ const config = parseSidecarConfig(parsed);
88
+ if (config && !("type" in (parsed as Record<string, unknown>))) {
89
+ void applyConfig(state, config, stderr);
90
+ return;
91
+ }
92
+
93
+ const message = parseNvimSidecarMessage(parsed);
94
+ if (!message) {
95
+ stderr.write(`pi-x-ide nvim sidecar: ignored malformed message: ${trimmed}\n`);
96
+ return;
97
+ }
98
+ void handleMessage(state, server, message, stderr, stop);
99
+ });
100
+ rl.on("close", () => void stop());
101
+
102
+ const cleanup = () => void stop();
103
+ process.once("SIGINT", cleanup);
104
+ process.once("SIGTERM", cleanup);
105
+ process.once("beforeExit", cleanup);
106
+
107
+ return { server, lockFilePath: state.lockFilePath, stop };
108
+ }
109
+
110
+ async function applyConfig(
111
+ state: RuntimeState,
112
+ config: { workspaceFolders?: string[] },
113
+ stderr: NodeJS.WritableStream,
114
+ ) {
115
+ if (config.workspaceFolders) state.workspaceFolders = normalizeWorkspaceFolders(config.workspaceFolders);
116
+ if (!state.lockFilePath || !state.lockFile) return;
117
+ state.lockFile = refreshIdeLockFile(state.lockFile, state.workspaceFolders);
118
+ try {
119
+ await writeIdeLockFile(state.lockFilePath, state.lockFile);
120
+ } catch (error) {
121
+ stderr.write(`pi-x-ide nvim sidecar: failed to refresh lock file: ${errorMessage(error)}\n`);
122
+ }
123
+ }
124
+
125
+ async function handleMessage(
126
+ state: RuntimeState,
127
+ server: IdeWebSocketServer,
128
+ message: NvimSidecarMessage,
129
+ stderr: NodeJS.WritableStream,
130
+ stop: () => Promise<void>,
131
+ ): Promise<void> {
132
+ switch (message.type) {
133
+ case "selection_changed": {
134
+ const snapshot = withNvimSource(message.snapshot);
135
+ state.latestSelection = snapshot;
136
+ server.broadcast({
137
+ jsonrpc: "2.0",
138
+ method: "selection_changed",
139
+ params: { ...snapshot, receivedAt: snapshot.receivedAt ?? Date.now() },
140
+ });
141
+ return;
142
+ }
143
+ case "selection_cleared":
144
+ state.latestSelection = undefined;
145
+ server.broadcast({
146
+ jsonrpc: "2.0",
147
+ method: "selection_cleared",
148
+ params: { source: "nvim", reason: message.reason ?? "no-active-editor", receivedAt: Date.now() },
149
+ });
150
+ return;
151
+ case "at_mentioned": {
152
+ const snapshot = withNvimSource(message.snapshot);
153
+ state.latestSelection = snapshot;
154
+ const rangeText = message.rangeText || formatRangeMention(snapshot);
155
+ server.broadcast({
156
+ jsonrpc: "2.0",
157
+ method: "at_mentioned",
158
+ params: { ...snapshot, rangeText, receivedAt: snapshot.receivedAt ?? Date.now() },
159
+ });
160
+ return;
161
+ }
162
+ case "workspace_changed":
163
+ await applyConfig(state, { workspaceFolders: message.workspaceFolders }, stderr);
164
+ return;
165
+ case "shutdown":
166
+ await stop();
167
+ return;
168
+ }
169
+ }
170
+
171
+ function withNvimSource<T extends EditorSelectionSnapshot>(snapshot: T): T {
172
+ return { ...snapshot, source: "nvim" };
173
+ }
174
+
175
+ function normalizeWorkspaceFolders(workspaceFolders: string[] | undefined): string[] {
176
+ const folders = workspaceFolders?.filter((folder) => folder.length > 0) ?? [];
177
+ return folders.length > 0 ? folders : [process.cwd()];
178
+ }
179
+
180
+ function errorMessage(error: unknown): string {
181
+ return error instanceof Error ? error.message : String(error);
182
+ }
183
+
184
+ function parseCliArgs(argv: string[]): NvimSidecarOptions | "help" {
185
+ const options: NvimSidecarOptions = {};
186
+ for (let index = 0; index < argv.length; index += 1) {
187
+ const arg = argv[index];
188
+ if (arg === "--help" || arg === "-h") return "help";
189
+ if (arg === "--name") options.name = argv[++index];
190
+ else if (arg === "--lock-dir") options.lockDir = argv[++index];
191
+ else if (arg === "--workspace-folder") {
192
+ options.workspaceFolders = [...(options.workspaceFolders ?? []), argv[++index]].filter(Boolean);
193
+ } else if (arg === "--workspace-folders") {
194
+ const value = argv[++index];
195
+ const parsed = parseJsonLine(value ?? "");
196
+ const config = parseSidecarConfig({ workspaceFolders: parsed });
197
+ if (config?.workspaceFolders) options.workspaceFolders = config.workspaceFolders;
198
+ }
199
+ }
200
+ return options;
201
+ }
202
+
203
+ function printHelp(): void {
204
+ process.stdout.write(
205
+ `Usage: pi-x-ide-nvim-sidecar [options]\n\nOptions:\n --workspace-folder <path> Add a workspace folder. May be repeated.\n --workspace-folders <json> JSON array of workspace folders.\n --lock-dir <path> Override PI_X_IDE_LOCK_DIR for tests.\n --name <name> Display name reported to Pi.\n --help Show this help.\n`,
206
+ );
207
+ }
208
+
209
+ if (require.main === module) {
210
+ const parsed = parseCliArgs(process.argv.slice(2));
211
+ if (parsed === "help") {
212
+ printHelp();
213
+ } else {
214
+ startNvimSidecar(parsed).catch((error: unknown) => {
215
+ process.stderr.write(`pi-x-ide nvim sidecar: ${errorMessage(error)}\n`);
216
+ process.exitCode = 1;
217
+ });
218
+ }
219
+ }
package/src/pi/index.ts CHANGED
@@ -91,10 +91,20 @@ async function maybeAutoInstallAndReconnect(
91
91
 
92
92
  notifyInstall(ctx, `Pi x IDE extension installed for ${candidate.label}. Trying to connect...`, "info");
93
93
  const connected = await retryConnectAfterInstall(runtime, ctx, generation);
94
- if (!connected && isInstallSessionActive(runtime, generation)) {
94
+ if (!isInstallSessionActive(runtime, generation)) return;
95
+
96
+ if (connected && runtime.connectionStatus === "connected") {
97
+ notifyInstall(ctx, `Pi x IDE extension installed for ${candidate.label}. Connected!`, "info");
98
+ } else if (!connected) {
99
+ notifyInstall(
100
+ ctx,
101
+ `Pi x IDE extension installed for ${candidate.label}. If Pi does not connect automatically, reload the IDE window and run /ide.`,
102
+ "warning",
103
+ );
104
+ } else {
95
105
  notifyInstall(
96
106
  ctx,
97
- `Pi x IDE extension installed for ${candidate.label}. If Pi does not connect automatically, reload the IDE window and run /ide auto.`,
107
+ `Pi x IDE extension installed for ${candidate.label}. Connection attempt completed. Run /ide to retry if needed.`,
98
108
  "warning",
99
109
  );
100
110
  }
package/src/pi/install.ts CHANGED
@@ -177,6 +177,14 @@ export async function runCli(
177
177
  args: string[],
178
178
  timeoutMs = 15_000,
179
179
  ): Promise<{ stdout: string; stderr: string }> {
180
+ if (process.platform === "win32" && isCmdOrBatFile(cliPath)) {
181
+ const { stdout, stderr } = await execFileAsync(resolveCmdExe(), ["/d", "/c", cliPath, ...args], {
182
+ timeout: timeoutMs,
183
+ windowsHide: true,
184
+ maxBuffer: 1024 * 1024,
185
+ });
186
+ return { stdout: String(stdout), stderr: String(stderr) };
187
+ }
180
188
  const { stdout, stderr } = await execFileAsync(cliPath, args, {
181
189
  timeout: timeoutMs,
182
190
  windowsHide: true,
@@ -304,7 +312,13 @@ function parsePathExt(env: NodeJS.ProcessEnv): string[] {
304
312
  .split(";")
305
313
  .map((extension) => extension.trim())
306
314
  .filter(Boolean);
307
- return values.length > 0 ? ["", ...values] : [""];
315
+ // On Windows, skip extensionless search to avoid matching
316
+ // non-executable shell scripts (e.g., VS Code ships both
317
+ // `code` (sh) and `code.cmd` — only `.cmd` is executable).
318
+ if (process.platform !== "win32") {
319
+ return values.length > 0 ? ["", ...values] : [""];
320
+ }
321
+ return values;
308
322
  }
309
323
 
310
324
  function withExtension(command: string, extension: string): string {
@@ -312,6 +326,15 @@ function withExtension(command: string, extension: string): string {
312
326
  return command.toLowerCase().endsWith(extension.toLowerCase()) ? command : `${command}${extension}`;
313
327
  }
314
328
 
329
+ function isCmdOrBatFile(cliPath: string): boolean {
330
+ const lowerPath = cliPath.toLowerCase();
331
+ return lowerPath.endsWith(".cmd") || lowerPath.endsWith(".bat");
332
+ }
333
+
334
+ function resolveCmdExe(): string {
335
+ return process.env.ComSpec ?? "cmd.exe";
336
+ }
337
+
315
338
  function getExecOutput(error: unknown, key: "stdout" | "stderr"): string {
316
339
  if (typeof error !== "object" || error === null || !(key in error)) return "";
317
340
  const value = (error as Record<typeof key, unknown>)[key];
@@ -0,0 +1,120 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import WebSocket, { WebSocketServer } from "ws";
3
+ import {
4
+ AUTH_HEADER,
5
+ PROTOCOL_VERSION,
6
+ type EditorSelectionSnapshot,
7
+ type IdeSource,
8
+ type InitializeResult,
9
+ } from "./protocol";
10
+ import { isJsonRpcRequest } from "./schema";
11
+ import { decodeRawData } from "./ws";
12
+
13
+ export class IdeWebSocketServer {
14
+ private httpServer?: Server;
15
+ private wss?: WebSocketServer;
16
+ private readonly sockets = new Set<WebSocket>();
17
+
18
+ constructor(
19
+ private readonly authToken: string,
20
+ private readonly serverInfo: { name: string; version?: string; ide?: IdeSource },
21
+ private readonly getInitialSelection?: () => EditorSelectionSnapshot | undefined,
22
+ ) {}
23
+
24
+ get port(): number {
25
+ const address = this.httpServer?.address();
26
+ if (!address || typeof address === "string") return 0;
27
+ return address.port;
28
+ }
29
+
30
+ get clientCount(): number {
31
+ return this.sockets.size;
32
+ }
33
+
34
+ async start(): Promise<number> {
35
+ this.httpServer = createServer();
36
+ this.wss = new WebSocketServer({
37
+ server: this.httpServer,
38
+ verifyClient: ({ req }, done) => {
39
+ const header = req.headers[AUTH_HEADER];
40
+ const token = Array.isArray(header) ? header[0] : header;
41
+ done(token === this.authToken, token === this.authToken ? undefined : 401, "Unauthorized");
42
+ },
43
+ });
44
+
45
+ this.wss.on("connection", (socket) => {
46
+ this.sockets.add(socket);
47
+ socket.on("close", () => this.sockets.delete(socket));
48
+ socket.on("error", () => this.sockets.delete(socket));
49
+ socket.on("message", (raw) => this.handleMessage(socket, decodeRawData(raw)));
50
+ });
51
+
52
+ await new Promise<void>((resolve, reject) => {
53
+ this.httpServer!.once("error", reject);
54
+ this.httpServer!.listen(0, "127.0.0.1", () => {
55
+ this.httpServer!.off("error", reject);
56
+ resolve();
57
+ });
58
+ });
59
+
60
+ return this.port;
61
+ }
62
+
63
+ broadcast(value: unknown): void {
64
+ const text = JSON.stringify(value);
65
+ for (const socket of this.sockets) {
66
+ if (socket.readyState === WebSocket.OPEN) socket.send(text);
67
+ }
68
+ }
69
+
70
+ async stop(): Promise<void> {
71
+ for (const socket of this.sockets) socket.close();
72
+ this.sockets.clear();
73
+ await Promise.all([
74
+ new Promise<void>((resolve) => this.wss?.close(() => resolve()) ?? resolve()),
75
+ new Promise<void>((resolve) => this.httpServer?.close(() => resolve()) ?? resolve()),
76
+ ]);
77
+ }
78
+
79
+ private handleMessage(socket: WebSocket, text: string): void {
80
+ let parsed: unknown;
81
+ try {
82
+ parsed = JSON.parse(text) as unknown;
83
+ } catch {
84
+ return;
85
+ }
86
+
87
+ if (!isJsonRpcRequest(parsed)) return;
88
+ if (parsed.method !== "initialize") return;
89
+
90
+ const ide = this.serverInfo.ide ?? "vscode";
91
+ const result: InitializeResult = {
92
+ protocolVersion: PROTOCOL_VERSION,
93
+ server: {
94
+ name: this.serverInfo.name,
95
+ version: this.serverInfo.version,
96
+ ide,
97
+ },
98
+ };
99
+
100
+ socket.send(JSON.stringify({ jsonrpc: "2.0", id: parsed.id, result }));
101
+
102
+ const snapshot = this.getInitialSelection?.();
103
+ socket.send(
104
+ JSON.stringify({
105
+ jsonrpc: "2.0",
106
+ method: snapshot ? "selection_changed" : "selection_cleared",
107
+ params: snapshot
108
+ ? {
109
+ ...snapshot,
110
+ receivedAt: Date.now(),
111
+ }
112
+ : {
113
+ source: ide,
114
+ reason: "no-active-editor",
115
+ receivedAt: Date.now(),
116
+ },
117
+ }),
118
+ );
119
+ }
120
+ }
@@ -0,0 +1,65 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { LOCK_FILE_EXTENSION, type IdeLockFile, type IdeSource } from "./protocol";
5
+ import { resolveLockDir } from "./paths";
6
+
7
+ export function createAuthToken(): string {
8
+ return randomBytes(32).toString("hex");
9
+ }
10
+
11
+ export function createIdeLockFilePath(source: IdeSource, port: number, pid = process.pid): string {
12
+ return join(resolveLockDir(), `${source}-${pid}-${port}${LOCK_FILE_EXTENSION}`);
13
+ }
14
+
15
+ export interface CreateIdeLockFileOptions {
16
+ ide: IdeSource;
17
+ name: string;
18
+ port: number;
19
+ authToken: string;
20
+ workspaceFolders: string[];
21
+ pid?: number;
22
+ now?: Date;
23
+ }
24
+
25
+ export function createIdeLockFile(options: CreateIdeLockFileOptions): IdeLockFile {
26
+ const now = (options.now ?? new Date()).toISOString();
27
+ return {
28
+ version: 1,
29
+ ide: options.ide,
30
+ name: options.name,
31
+ transport: "ws",
32
+ host: "127.0.0.1",
33
+ port: options.port,
34
+ authToken: options.authToken,
35
+ workspaceFolders: options.workspaceFolders,
36
+ pid: options.pid ?? process.pid,
37
+ createdAt: now,
38
+ updatedAt: now,
39
+ };
40
+ }
41
+
42
+ export async function writeIdeLockFile(path: string, lock: IdeLockFile): Promise<void> {
43
+ const dir = resolveLockDir();
44
+ await mkdir(dir, { recursive: true, mode: 0o700 });
45
+ await chmod(dir, 0o700).catch(() => undefined);
46
+
47
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
48
+ await writeFile(tmp, `${JSON.stringify(lock, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
49
+ await chmod(tmp, 0o600).catch(() => undefined);
50
+ await rename(tmp, path);
51
+ await chmod(path, 0o600).catch(() => undefined);
52
+ }
53
+
54
+ export async function removeIdeLockFile(path: string | undefined): Promise<void> {
55
+ if (!path) return;
56
+ await rm(path, { force: true }).catch(() => undefined);
57
+ }
58
+
59
+ export function refreshIdeLockFile(lock: IdeLockFile, workspaceFolders: string[], now = new Date()): IdeLockFile {
60
+ return {
61
+ ...lock,
62
+ workspaceFolders,
63
+ updatedAt: now.toISOString(),
64
+ };
65
+ }
@@ -10,7 +10,14 @@ export function resolveLockDir(env: NodeJS.ProcessEnv = process.env): string {
10
10
  }
11
11
 
12
12
  export function normalizePath(input: string): string {
13
- return resolve(input);
13
+ const resolved = resolve(input);
14
+ // Normalize drive letter to uppercase on Windows so that path comparison
15
+ // is case-insensitive (VS Code may write workspace paths with lowercase
16
+ // drive letters while process.cwd() uses uppercase).
17
+ if (process.platform === "win32" && resolved.length >= 2 && resolved[1] === ":") {
18
+ return resolved[0].toUpperCase() + resolved.slice(1);
19
+ }
20
+ return resolved;
14
21
  }
15
22
 
16
23
  export function isPathInsideOrEqual(parent: string, child: string): boolean {
@@ -3,7 +3,7 @@ export const LOCK_DIR_ENV = "PI_X_IDE_LOCK_DIR";
3
3
  export const AUTH_HEADER = "x-pi-x-ide-authorization";
4
4
  export const LOCK_FILE_EXTENSION = ".lock";
5
5
 
6
- export type IdeSource = "vscode" | "zed" | "unknown";
6
+ export type IdeSource = "vscode" | "zed" | "nvim" | "unknown";
7
7
  export type Transport = "ws";
8
8
 
9
9
  export interface IdeLockFile {
@@ -22,7 +22,7 @@ function isFiniteNumber(value: unknown): value is number {
22
22
  }
23
23
 
24
24
  function isIdeSource(value: unknown): value is IdeSource {
25
- return value === "vscode" || value === "zed" || value === "unknown";
25
+ return value === "vscode" || value === "zed" || value === "nvim" || value === "unknown";
26
26
  }
27
27
 
28
28
  export function isIdeLockFile(value: unknown): value is IdeLockFile {