@saifymatteo/pi-x-ide 0.0.0-stage → 1.20.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 (81) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/CONTRIBUTING.md +119 -0
  3. package/CONTRIBUTING.zh-CN.md +106 -0
  4. package/LICENSE +201 -0
  5. package/README.md +33 -2
  6. package/README.zh-CN.md +34 -0
  7. package/RELEASE.md +213 -0
  8. package/config.example.json +9 -0
  9. package/dist/src/pi/chunks/chunk-4RHJAL4S.js +200 -0
  10. package/dist/src/pi/chunks/chunk-UUXVCTOS.js +819 -0
  11. package/dist/src/pi/chunks/config-ui-LO3DVQFC.js +8073 -0
  12. package/dist/src/pi/chunks/runtime-services-BVQBAAWT.js +19569 -0
  13. package/dist/src/pi/commands.js +126 -0
  14. package/dist/src/pi/config-ui.js +127 -0
  15. package/dist/src/pi/connection.js +237 -0
  16. package/dist/src/pi/context.js +30 -0
  17. package/dist/src/pi/diagnostic-renderer.js +326 -0
  18. package/dist/src/pi/diagnostic-types.js +1 -0
  19. package/dist/src/pi/diagnostics.js +104 -0
  20. package/dist/src/pi/discovery.js +135 -0
  21. package/dist/src/pi/effect-boundary.js +5 -0
  22. package/dist/src/pi/ide-host.js +68 -0
  23. package/dist/src/pi/index.js +636 -0
  24. package/dist/src/pi/install.js +308 -0
  25. package/dist/src/pi/reconnect.js +57 -0
  26. package/dist/src/pi/runtime-loader.js +38 -0
  27. package/dist/src/pi/runtime-services.js +356 -0
  28. package/dist/src/pi/safety.js +61 -0
  29. package/dist/src/pi/selection.js +18 -0
  30. package/dist/src/pi/state.js +13 -0
  31. package/dist/src/pi/ui.js +304 -0
  32. package/dist/src/pi/zed.js +409 -0
  33. package/dist/src/shared/config-options.js +45 -0
  34. package/dist/src/shared/config.js +183 -0
  35. package/dist/src/shared/display-width.js +99 -0
  36. package/dist/src/shared/effect-errors.js +36 -0
  37. package/dist/src/shared/effect-runtime.js +36 -0
  38. package/dist/src/shared/effect-schema.js +120 -0
  39. package/dist/src/shared/errors.js +48 -0
  40. package/dist/src/shared/format.js +92 -0
  41. package/dist/src/shared/ide-server.js +167 -0
  42. package/dist/src/shared/jsonrpc-guard.js +12 -0
  43. package/dist/src/shared/lock-file-heartbeat.js +58 -0
  44. package/dist/src/shared/lock-file.js +50 -0
  45. package/dist/src/shared/paths.js +79 -0
  46. package/dist/src/shared/platform.js +57 -0
  47. package/dist/src/shared/protocol.js +3 -0
  48. package/dist/src/shared/schema.js +37 -0
  49. package/dist/src/shared/version.js +1 -0
  50. package/dist/src/shared/ws.js +8 -0
  51. package/docs/explanation/architecture.md +123 -0
  52. package/docs/explanation/discovery.md +113 -0
  53. package/docs/explanation/effect.md +127 -0
  54. package/docs/how-to/install-jetbrains.md +58 -0
  55. package/docs/how-to/install-neovim.md +138 -0
  56. package/docs/how-to/install-vscode.md +75 -0
  57. package/docs/how-to/install-zed.md +70 -0
  58. package/docs/how-to/troubleshoot-connection.md +103 -0
  59. package/docs/how-to/use-wsl2.md +90 -0
  60. package/docs/reference/commands.md +59 -0
  61. package/docs/reference/configuration.md +101 -0
  62. package/docs/reference/feature-parity.md +26 -0
  63. package/docs/reference/protocol.md +71 -0
  64. package/docs/specs/ide-protocol.md +215 -0
  65. package/docs/tutorials/get-started.md +133 -0
  66. package/docs/zh-CN/explanation/architecture.md +93 -0
  67. package/docs/zh-CN/explanation/discovery.md +71 -0
  68. package/docs/zh-CN/explanation/effect.md +90 -0
  69. package/docs/zh-CN/how-to/install-jetbrains.md +49 -0
  70. package/docs/zh-CN/how-to/install-neovim.md +126 -0
  71. package/docs/zh-CN/how-to/install-vscode.md +67 -0
  72. package/docs/zh-CN/how-to/install-zed.md +56 -0
  73. package/docs/zh-CN/how-to/troubleshoot-connection.md +85 -0
  74. package/docs/zh-CN/how-to/use-wsl2.md +75 -0
  75. package/docs/zh-CN/reference/commands.md +54 -0
  76. package/docs/zh-CN/reference/configuration.md +94 -0
  77. package/docs/zh-CN/reference/feature-parity.md +21 -0
  78. package/docs/zh-CN/reference/protocol.md +54 -0
  79. package/docs/zh-CN/tutorials/get-started.md +101 -0
  80. package/package.json +86 -4
  81. package/schemas/config.json +77 -0
@@ -0,0 +1,409 @@
1
+ import { copyFileSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Fiber from "effect/Fiber";
7
+ import { resolvePiConfigEnv } from "../shared/config.js";
8
+ import { isWsl, normalizePathForHost } from "../shared/platform.js";
9
+ import { snapshotKey } from "../shared/format.js";
10
+ import { isPathInsideOrEqual } from "../shared/paths.js";
11
+ import { clearLatestSelection, setLatestSelection } from "./selection.js";
12
+ import { containPiError } from "./safety.js";
13
+ import { updateIdeUi } from "./ui.js";
14
+ export { isWsl };
15
+ export const PI_X_IDE_ZED_DB_ENV = "PI_X_IDE_ZED_DB";
16
+ export const PI_X_IDE_ZED_POLL_INTERVAL_MS_ENV = "PI_X_IDE_ZED_POLL_INTERVAL_MS";
17
+ export const ZED_POLL_INTERVAL_MS = 1000;
18
+ export const ZED_POLL_INTERVAL_MS_MIN = 100;
19
+ export const ZED_POLL_INTERVAL_MS_MAX = 2000;
20
+ export function isZedTerminal(env = process.env) {
21
+ const configuredEnv = resolvePiConfigEnv(env);
22
+ return configuredEnv.ZED_TERM === "true" || configuredEnv.TERM_PROGRAM?.toLowerCase() === "zed";
23
+ }
24
+ export function normalizeZedPathForHost(input, env = process.env) {
25
+ return normalizePathForHost(input, env);
26
+ }
27
+ export function resolveZedDbPath(env = process.env, home = homedir()) {
28
+ const configuredEnv = resolvePiConfigEnv(env);
29
+ const override = configuredEnv[PI_X_IDE_ZED_DB_ENV]?.trim();
30
+ if (override) {
31
+ const normalizedOverride = normalizeZedPathForHost(override, configuredEnv);
32
+ return isFile(normalizedOverride) ? normalizedOverride : undefined;
33
+ }
34
+ const candidates = [
35
+ resolve(home, ".local", "share", "zed", "db", "0-stable", "db.sqlite"),
36
+ resolve(home, "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"),
37
+ resolve(home, "AppData", "Local", "Zed", "db", "0-stable", "db.sqlite"),
38
+ ...zedDbCandidatesFromWindowsEnv(configuredEnv),
39
+ ...zedDbCandidatesFromWslMount(configuredEnv),
40
+ ];
41
+ return candidates.find(isFile);
42
+ }
43
+ function zedDbCandidatesFromWindowsEnv(env) {
44
+ const localAppData = env.LOCALAPPDATA?.trim();
45
+ if (localAppData)
46
+ return [resolve(normalizeZedPathForHost(localAppData, env), "Zed", "db", "0-stable", "db.sqlite")];
47
+ const userProfile = env.USERPROFILE?.trim();
48
+ if (userProfile) {
49
+ return [
50
+ resolve(normalizeZedPathForHost(userProfile, env), "AppData", "Local", "Zed", "db", "0-stable", "db.sqlite"),
51
+ ];
52
+ }
53
+ return [];
54
+ }
55
+ function zedDbCandidatesFromWslMount(env) {
56
+ if (!isWsl(env))
57
+ return [];
58
+ const usersRoot = "/mnt/c/Users";
59
+ try {
60
+ return readdirSync(usersRoot, { withFileTypes: true })
61
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
62
+ .map((entry) => resolve(usersRoot, entry.name, "AppData", "Local", "Zed", "db", "0-stable", "db.sqlite"));
63
+ }
64
+ catch {
65
+ return [];
66
+ }
67
+ }
68
+ function isFile(path) {
69
+ try {
70
+ return statSync(path).isFile();
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ function openZedDatabase(dbPath, env = process.env) {
77
+ const configuredEnv = resolvePiConfigEnv(env);
78
+ if (isWsl(configuredEnv))
79
+ return openZedDatabaseSnapshot(dbPath);
80
+ try {
81
+ return { db: new DatabaseSync(dbPath, { readOnly: true }), cleanup: () => undefined };
82
+ }
83
+ catch {
84
+ return openZedDatabaseSnapshot(dbPath);
85
+ }
86
+ }
87
+ function openZedDatabaseSnapshot(dbPath) {
88
+ let snapshotDir;
89
+ try {
90
+ snapshotDir = mkdtempSync(join(tmpdir(), "pi-x-ide-zed-db-"));
91
+ const cleanupDir = snapshotDir;
92
+ const snapshotPath = join(cleanupDir, "db.sqlite");
93
+ copyFileSync(dbPath, snapshotPath);
94
+ for (const suffix of ["-wal", "-shm"]) {
95
+ const sidecarPath = `${dbPath}${suffix}`;
96
+ if (isFile(sidecarPath))
97
+ copyFileSync(sidecarPath, `${snapshotPath}${suffix}`);
98
+ }
99
+ try {
100
+ const dbRW = new DatabaseSync(snapshotPath);
101
+ dbRW.exec("PRAGMA wal_checkpoint(TRUNCATE)");
102
+ dbRW.close();
103
+ }
104
+ catch {
105
+ }
106
+ return {
107
+ db: new DatabaseSync(snapshotPath, { readOnly: true }),
108
+ cleanup: () => rmSync(cleanupDir, { recursive: true, force: true }),
109
+ };
110
+ }
111
+ catch {
112
+ if (snapshotDir)
113
+ rmSync(snapshotDir, { recursive: true, force: true });
114
+ return undefined;
115
+ }
116
+ }
117
+ export function parseZedWorkspacePaths(value) {
118
+ if (!value)
119
+ return [];
120
+ const trimmed = value.trim();
121
+ if (!trimmed)
122
+ return [];
123
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
124
+ try {
125
+ const parsed = JSON.parse(trimmed);
126
+ return Array.isArray(parsed)
127
+ ? parsed.filter((item) => typeof item === "string" && item.length > 0)
128
+ : [];
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ }
134
+ return trimmed
135
+ .split(/\r?\n/)
136
+ .map((line) => line.trim())
137
+ .filter((line) => line.length > 0);
138
+ }
139
+ function utf8ByteOffsetToStringIndex(text, byteOffset) {
140
+ if (byteOffset <= 0)
141
+ return 0;
142
+ const encoder = new TextEncoder();
143
+ let bytes = 0;
144
+ for (let index = 0; index < text.length;) {
145
+ const codePoint = text.codePointAt(index);
146
+ if (codePoint === undefined)
147
+ return text.length;
148
+ const nextIndex = index + (codePoint > 0xffff ? 2 : 1);
149
+ bytes += encoder.encode(text.slice(index, nextIndex)).length;
150
+ if (bytes >= byteOffset)
151
+ return nextIndex;
152
+ index = nextIndex;
153
+ }
154
+ return text.length;
155
+ }
156
+ function byteOffsetToSelection(text, startByte, endByte) {
157
+ const startIndex = utf8ByteOffsetToStringIndex(text, startByte);
158
+ const endIndex = utf8ByteOffsetToStringIndex(text, endByte);
159
+ const lines = text.split("\n");
160
+ let startLine = 0;
161
+ let startChar = 0;
162
+ let remaining = startIndex;
163
+ for (let lineIdx = 0; lineIdx < lines.length && remaining >= 0; lineIdx += 1) {
164
+ const lineLen = lines[lineIdx].length + 1;
165
+ if (remaining <= lines[lineIdx].length) {
166
+ startLine = lineIdx;
167
+ startChar = remaining;
168
+ break;
169
+ }
170
+ remaining -= lineLen;
171
+ }
172
+ let endLine = 0;
173
+ let endChar = 0;
174
+ remaining = endIndex;
175
+ for (let lineIdx = 0; lineIdx < lines.length && remaining >= 0; lineIdx += 1) {
176
+ const lineLen = lines[lineIdx].length + 1;
177
+ if (remaining <= lines[lineIdx].length) {
178
+ endLine = lineIdx;
179
+ endChar = remaining;
180
+ break;
181
+ }
182
+ remaining -= lineLen;
183
+ }
184
+ return {
185
+ start: { line: startLine, character: startChar },
186
+ end: { line: endLine, character: endChar },
187
+ };
188
+ }
189
+ function scoreWorkspace(workspacePaths, cwd, env) {
190
+ const normalizedCwd = normalizeZedPathForHost(cwd, env);
191
+ return parseZedWorkspacePaths(workspacePaths).reduce((best, workspacePath) => {
192
+ const normalizedWorkspacePath = normalizeZedPathForHost(workspacePath, env);
193
+ if (isPathInsideOrEqual(normalizedWorkspacePath, normalizedCwd)) {
194
+ const resolved = resolve(normalizedWorkspacePath);
195
+ return Math.max(best, resolved.length);
196
+ }
197
+ return best;
198
+ }, 0);
199
+ }
200
+ export function resolveZedSelection(options) {
201
+ const { dbPath, cwd, readFile = (path) => readFileSync(path, "utf8"), env: inputEnv = process.env } = options;
202
+ const env = resolvePiConfigEnv(inputEnv);
203
+ const dbHandle = openZedDatabase(dbPath, env);
204
+ if (!dbHandle)
205
+ return undefined;
206
+ const { db, cleanup } = dbHandle;
207
+ try {
208
+ const rows = db
209
+ .prepare(`SELECT i.kind AS item_kind,
210
+ e.item_id AS editor_id,
211
+ i.workspace_id,
212
+ w.paths AS workspace_paths,
213
+ w.timestamp,
214
+ e.buffer_path
215
+ FROM items i
216
+ JOIN panes p ON p.pane_id = i.pane_id AND p.workspace_id = i.workspace_id
217
+ JOIN workspaces w ON w.workspace_id = i.workspace_id
218
+ LEFT JOIN editors e ON e.item_id = i.item_id AND e.workspace_id = i.workspace_id
219
+ WHERE i.active = 1 AND p.active = 1
220
+ ORDER BY w.timestamp DESC`)
221
+ .all();
222
+ const scored = rows
223
+ .filter((row) => row.item_kind === "Editor" && row.editor_id != null && typeof row.buffer_path === "string")
224
+ .map((row) => ({
225
+ ...row,
226
+ score: scoreWorkspace(row.workspace_paths, cwd, env),
227
+ }))
228
+ .filter((row) => row.score > 0);
229
+ if (scored.length === 0)
230
+ return undefined;
231
+ scored.sort((a, b) => b.score - a.score || b.timestamp - a.timestamp);
232
+ const best = scored[0];
233
+ if (!best)
234
+ return undefined;
235
+ const { editor_id, workspace_id, buffer_path, workspace_paths } = best;
236
+ const normalizedBufferPath = normalizeZedPathForHost(buffer_path, env);
237
+ const workspaceFolder = bestWorkspaceFolder(workspace_paths, cwd, env);
238
+ let contents;
239
+ const contentRow = db
240
+ .prepare("SELECT contents FROM editors WHERE item_id = ? AND workspace_id = ?")
241
+ .get(editor_id, workspace_id);
242
+ if (contentRow && typeof contentRow.contents === "string") {
243
+ contents = contentRow.contents;
244
+ }
245
+ else {
246
+ try {
247
+ contents = readFile(normalizedBufferPath);
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ }
253
+ if (contents === undefined)
254
+ return undefined;
255
+ const selectionRows = db
256
+ .prepare("SELECT start AS selection_start, end AS selection_end FROM editor_selections WHERE editor_id = ? AND workspace_id = ?")
257
+ .all(editor_id, workspace_id);
258
+ const ranges = [];
259
+ for (const sel of selectionRows) {
260
+ const rawStart = sel.selection_start;
261
+ const rawEnd = sel.selection_end;
262
+ const start = Math.min(rawStart, rawEnd);
263
+ const end = Math.max(rawStart, rawEnd);
264
+ if (start >= end)
265
+ continue;
266
+ const text = contents.slice(utf8ByteOffsetToStringIndex(contents, start), utf8ByteOffsetToStringIndex(contents, end));
267
+ if (!text)
268
+ continue;
269
+ const selection = byteOffsetToSelection(contents, start, end);
270
+ ranges.push({ text, selection });
271
+ }
272
+ return {
273
+ source: "zed",
274
+ filePath: normalizedBufferPath,
275
+ workspaceFolder,
276
+ ranges,
277
+ receivedAt: options.now ?? Date.now(),
278
+ };
279
+ }
280
+ catch {
281
+ return undefined;
282
+ }
283
+ finally {
284
+ try {
285
+ db.close();
286
+ }
287
+ finally {
288
+ cleanup();
289
+ }
290
+ }
291
+ }
292
+ function bestWorkspaceFolder(workspacePaths, cwd, env) {
293
+ const paths = parseZedWorkspacePaths(workspacePaths).map((workspacePath) => normalizeZedPathForHost(workspacePath, env));
294
+ const normalizedCwd = normalizeZedPathForHost(cwd, env);
295
+ const matches = paths.filter((workspacePath) => isPathInsideOrEqual(workspacePath, normalizedCwd));
296
+ if (matches.length === 0)
297
+ return paths[0];
298
+ return matches.sort((a, b) => resolve(b).length - resolve(a).length)[0];
299
+ }
300
+ export function resolveZedPollIntervalMs(env = process.env, intervalMs) {
301
+ const configuredEnv = resolvePiConfigEnv(env);
302
+ const configuredIntervalRaw = configuredEnv[PI_X_IDE_ZED_POLL_INTERVAL_MS_ENV];
303
+ let configuredInterval;
304
+ if (configuredIntervalRaw !== undefined) {
305
+ const parsed = Number(configuredIntervalRaw);
306
+ if (Number.isFinite(parsed) && parsed > 0)
307
+ configuredInterval = parsed;
308
+ }
309
+ const resolved = intervalMs ?? configuredInterval ?? ZED_POLL_INTERVAL_MS;
310
+ return Math.min(Math.max(resolved, ZED_POLL_INTERVAL_MS_MIN), ZED_POLL_INTERVAL_MS_MAX);
311
+ }
312
+ export function stopZedPolling(runtime) {
313
+ const fiber = runtime.zedPollFiber;
314
+ runtime.zedPollFiber = undefined;
315
+ runtime.zedPollIntervalMs = undefined;
316
+ if (fiber) {
317
+ Effect.runFork(Fiber.interrupt(fiber));
318
+ }
319
+ runtime.zedPollSelectionKey = undefined;
320
+ runtime.zedPollWalMtimeMs = undefined;
321
+ }
322
+ export function startZedPolling(runtime, ctx, options) {
323
+ const env = resolvePiConfigEnv(options?.env ?? process.env);
324
+ if (!isZedTerminal(env))
325
+ return false;
326
+ const dbPath = options?.dbPath ?? resolveZedDbPath(env);
327
+ if (!dbPath)
328
+ return false;
329
+ const intervalMs = resolveZedPollIntervalMs(env, options?.intervalMs);
330
+ const generation = options?.generation;
331
+ stopZedPolling(runtime);
332
+ runtime.connection?.disconnect();
333
+ runtime.connection = undefined;
334
+ runtime.currentCandidate = undefined;
335
+ runtime.connectionStatus = "connected";
336
+ runtime.connectedServer = { name: "Zed", ide: "zed" };
337
+ runtime.connectionMessage = undefined;
338
+ runtime.zedPollIntervalMs = intervalMs;
339
+ updateIdeUi(runtime, ctx);
340
+ const pollTick = () => {
341
+ if (generation !== undefined && runtime.sessionGeneration !== generation)
342
+ return "stop";
343
+ if (runtime.connection && runtime.connectedServer?.ide !== "zed")
344
+ return "stop";
345
+ const walPath = `${dbPath}-wal`;
346
+ let walMtimeMs;
347
+ try {
348
+ walMtimeMs = statSync(walPath).mtimeMs;
349
+ }
350
+ catch {
351
+ }
352
+ if (walMtimeMs !== undefined &&
353
+ runtime.zedPollWalMtimeMs !== undefined &&
354
+ walMtimeMs === runtime.zedPollWalMtimeMs) {
355
+ return "continue";
356
+ }
357
+ runtime.zedPollWalMtimeMs = walMtimeMs;
358
+ let snapshot;
359
+ try {
360
+ snapshot = resolveZedSelection({ dbPath, cwd: ctx.cwd, env });
361
+ }
362
+ catch {
363
+ snapshot = undefined;
364
+ }
365
+ if (snapshot) {
366
+ const key = snapshotKey(snapshot);
367
+ if (key !== runtime.zedPollSelectionKey) {
368
+ runtime.zedPollSelectionKey = key;
369
+ setLatestSelection(runtime, snapshot, ctx);
370
+ }
371
+ }
372
+ else {
373
+ clearLatestSelection(runtime, ctx);
374
+ }
375
+ return "continue";
376
+ };
377
+ const program = Effect.gen(function* () {
378
+ while (true) {
379
+ yield* Effect.sleep(`${intervalMs} millis`);
380
+ let outcome;
381
+ let pollError;
382
+ try {
383
+ outcome = pollTick();
384
+ }
385
+ catch (error) {
386
+ outcome = "error";
387
+ pollError = error;
388
+ }
389
+ if (outcome === "error") {
390
+ runtime.zedPollFiber = undefined;
391
+ runtime.zedPollIntervalMs = undefined;
392
+ runtime.zedPollSelectionKey = undefined;
393
+ runtime.zedPollWalMtimeMs = undefined;
394
+ containPiError(runtime, "Zed polling", pollError, ctx);
395
+ return;
396
+ }
397
+ if (outcome === "stop")
398
+ return;
399
+ }
400
+ });
401
+ const fiber = Effect.runFork(program.pipe(Effect.ensuring(Effect.sync(() => {
402
+ if (runtime.zedPollFiber === fiber) {
403
+ runtime.zedPollFiber = undefined;
404
+ runtime.zedPollIntervalMs = undefined;
405
+ }
406
+ }))));
407
+ runtime.zedPollFiber = fiber;
408
+ return true;
409
+ }
@@ -0,0 +1,45 @@
1
+ export const CONFIG_ENV_VALUE_TYPES = ["string", "number", "boolean"];
2
+ export function isConfigEnvValue(value) {
3
+ return CONFIG_ENV_VALUE_TYPES.some((type) => typeof value === type);
4
+ }
5
+ export const CONFIG_ENV_OPTIONS = {
6
+ PI_X_IDE_AUTO_INSTALL: {
7
+ type: ["string", "number", "boolean"],
8
+ default: "enabled",
9
+ description: "Controls VS Code extension auto-install. Values 0, false, and off disable it.",
10
+ },
11
+ PI_X_IDE_ATTACH_SHORTCUT: {
12
+ type: ["string"],
13
+ default: "ctrl+alt+k",
14
+ description: "Pi TUI shortcut for attaching the latest IDE selection. Set to off, none, false, or 0 to disable.",
15
+ },
16
+ PI_X_IDE_HOST_OVERRIDE: {
17
+ type: ["string"],
18
+ description: "Override the host Pi uses to connect to IDE WebSocket lock files. Useful for WSL2 or custom networking.",
19
+ },
20
+ PI_X_IDE_ZED_DB: {
21
+ type: ["string"],
22
+ description: "Override path to Zed's SQLite database.",
23
+ },
24
+ PI_X_IDE_ZED_POLL_INTERVAL_MS: {
25
+ type: ["string", "number"],
26
+ description: "Polling interval in milliseconds when checking Zed's SQLite database for selection changes.",
27
+ default: "1000",
28
+ },
29
+ };
30
+ export const CONFIG_ENV_PATTERN_OPTIONS = [];
31
+ export const STATUS_DISPLAY_VALUES = ["widget", "statusline"];
32
+ export const DEFAULT_STATUS_DISPLAY = "widget";
33
+ export const CONFIG_OPTIONS = {
34
+ fixPrompt: {
35
+ type: ["string"],
36
+ default: "Analyze the errors and warnings at the following location, and try to fix them:\n{DIAGNOSTIC}",
37
+ description: "Custom prompt prefix when requesting a fix for IDE diagnostics. Use {DIAGNOSTIC} as a placeholder for the diagnostic context. If the placeholder is omitted, the diagnostic context is appended after your prompt.",
38
+ },
39
+ status_display: {
40
+ type: ["string"],
41
+ default: DEFAULT_STATUS_DISPLAY,
42
+ enum: STATUS_DISPLAY_VALUES,
43
+ description: 'Where to show IDE connection status. Use "widget" for the above-editor widget (default), or "statusline" for the default footer status line.',
44
+ },
45
+ };
@@ -0,0 +1,183 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, resolve } from "node:path";
4
+ import { DEFAULT_STATUS_DISPLAY, isConfigEnvValue, STATUS_DISPLAY_VALUES, } from "./config-options.js";
5
+ export const EXT_CONFIG_NAME = "pi-x-ide";
6
+ export const CONFIG_DIR_NAME = ".pi";
7
+ export const PI_CONFIG_FILE = "config.json";
8
+ export const STATUS_DISPLAY_CONFIG_KEY = "status_display";
9
+ export const AUTO_INSTALL_ENV_KEY = "PI_X_IDE_AUTO_INSTALL";
10
+ const processEnvOverlays = new WeakSet();
11
+ const AUTO_INSTALL_DISABLED_VALUES = new Set(["0", "false", "off"]);
12
+ export function resolvePiConfigPath(home = homedir()) {
13
+ return resolvePiGlobalConfigPath(home);
14
+ }
15
+ export function resolvePiGlobalConfigPath(home = homedir()) {
16
+ return resolve(home, CONFIG_DIR_NAME, EXT_CONFIG_NAME, PI_CONFIG_FILE);
17
+ }
18
+ export function resolvePiProjectConfigPath(projectDir) {
19
+ return resolve(projectDir, CONFIG_DIR_NAME, EXT_CONFIG_NAME, PI_CONFIG_FILE);
20
+ }
21
+ export function readPiConfigFile(configPath) {
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(readFileSync(configPath, "utf8"));
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ return isRecord(parsed) ? parsed : undefined;
30
+ }
31
+ export function writePiConfigFile(partial, configPath) {
32
+ const existing = readPiConfigFile(configPath) ?? {};
33
+ const merged = { ...existing, ...partial };
34
+ if (isRecord(existing.env) || isRecord(partial.env)) {
35
+ merged.env = {
36
+ ...(isRecord(existing.env) ? existing.env : {}),
37
+ ...(isRecord(partial.env) ? partial.env : {}),
38
+ };
39
+ }
40
+ mkdirSync(dirname(configPath), { recursive: true });
41
+ writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
42
+ }
43
+ export function readPiConfigEnv(configPath = resolvePiConfigPath()) {
44
+ const parsed = readPiConfigFile(configPath);
45
+ if (!parsed || !isRecord(parsed.env))
46
+ return {};
47
+ return Object.fromEntries(Object.entries(parsed.env)
48
+ .filter((entry) => {
49
+ const [key, value] = entry;
50
+ return key.length > 0 && isConfigEnvValue(value);
51
+ })
52
+ .map(([key, value]) => [key, String(value)]));
53
+ }
54
+ export function resolvePiConfigEnv(env = process.env, options = {}) {
55
+ if (env !== process.env && !options.configPath && options.projectDir === undefined) {
56
+ return env;
57
+ }
58
+ const configEnv = options.configPath
59
+ ? readPiConfigEnv(options.configPath)
60
+ : {
61
+ ...readPiConfigEnv(resolvePiGlobalConfigPath(options.home)),
62
+ ...(options.projectDir ? readPiConfigEnv(resolvePiProjectConfigPath(options.projectDir)) : {}),
63
+ };
64
+ if (Object.keys(configEnv).length === 0)
65
+ return env;
66
+ const merged = { ...configEnv, ...env };
67
+ if (env === process.env)
68
+ processEnvOverlays.add(merged);
69
+ return merged;
70
+ }
71
+ export function isProcessEnvOrPiConfigOverlay(env) {
72
+ return env === process.env || processEnvOverlays.has(env);
73
+ }
74
+ export function readPiConfigFixPrompt(configPath = resolvePiConfigPath()) {
75
+ const parsed = readPiConfigFile(configPath);
76
+ if (!parsed)
77
+ return undefined;
78
+ return typeof parsed.fixPrompt === "string" ? parsed.fixPrompt : undefined;
79
+ }
80
+ export function readPiConfigStatusDisplay(configPathOrOptions = {}) {
81
+ return resolvePiConfigStatusDisplay(configPathOrOptions).value;
82
+ }
83
+ export function resolvePiConfigStatusDisplay(configPathOrOptions = {}) {
84
+ if (typeof configPathOrOptions === "string") {
85
+ const value = readStatusDisplayFromFile(configPathOrOptions);
86
+ return value === undefined
87
+ ? { value: DEFAULT_STATUS_DISPLAY, scope: "default" }
88
+ : { value, scope: "global", path: configPathOrOptions };
89
+ }
90
+ const projectDir = configPathOrOptions.projectDir;
91
+ if (projectDir) {
92
+ const projectPath = resolvePiProjectConfigPath(projectDir);
93
+ const projectValue = readStatusDisplayFromFile(projectPath);
94
+ if (projectValue !== undefined) {
95
+ return { value: projectValue, scope: "project", path: projectPath };
96
+ }
97
+ }
98
+ const globalPath = resolvePiGlobalConfigPath(configPathOrOptions.home);
99
+ const globalValue = readStatusDisplayFromFile(globalPath);
100
+ if (globalValue !== undefined) {
101
+ return { value: globalValue, scope: "global", path: globalPath };
102
+ }
103
+ return { value: DEFAULT_STATUS_DISPLAY, scope: "default" };
104
+ }
105
+ export function resolvePiConfigAutoInstall(configPathOrOptions = {}) {
106
+ if (typeof configPathOrOptions === "string") {
107
+ const raw = readEnvValueFromFile(configPathOrOptions, AUTO_INSTALL_ENV_KEY);
108
+ return raw === undefined
109
+ ? { value: true, scope: "default" }
110
+ : { value: parseAutoInstallValue(raw), scope: "global", path: configPathOrOptions };
111
+ }
112
+ const projectDir = configPathOrOptions.projectDir;
113
+ if (projectDir) {
114
+ const projectPath = resolvePiProjectConfigPath(projectDir);
115
+ const projectRaw = readEnvValueFromFile(projectPath, AUTO_INSTALL_ENV_KEY);
116
+ if (projectRaw !== undefined) {
117
+ return { value: parseAutoInstallValue(projectRaw), scope: "project", path: projectPath };
118
+ }
119
+ }
120
+ const globalPath = resolvePiGlobalConfigPath(configPathOrOptions.home);
121
+ const globalRaw = readEnvValueFromFile(globalPath, AUTO_INSTALL_ENV_KEY);
122
+ if (globalRaw !== undefined) {
123
+ return { value: parseAutoInstallValue(globalRaw), scope: "global", path: globalPath };
124
+ }
125
+ return { value: true, scope: "default" };
126
+ }
127
+ export function resolveIdeConfigSettings(options = {}) {
128
+ const display = resolvePiConfigStatusDisplay(options);
129
+ const autoInstall = resolvePiConfigAutoInstall(options);
130
+ return {
131
+ settings: {
132
+ display: display.value,
133
+ autoInstall: autoInstall.value,
134
+ },
135
+ display,
136
+ autoInstall,
137
+ };
138
+ }
139
+ export function writePiConfigStatusDisplay(value, options) {
140
+ return writePiConfigSettings({ display: value }, options);
141
+ }
142
+ export function writePiConfigSettings(settings, options) {
143
+ const path = options.scope === "project"
144
+ ? resolvePiProjectConfigPath(requireProjectDir(options.projectDir))
145
+ : resolvePiGlobalConfigPath(options.home);
146
+ const partial = {};
147
+ if (settings.display !== undefined) {
148
+ partial[STATUS_DISPLAY_CONFIG_KEY] = settings.display;
149
+ }
150
+ if (settings.autoInstall !== undefined) {
151
+ partial.env = {
152
+ [AUTO_INSTALL_ENV_KEY]: settings.autoInstall ? "true" : "false",
153
+ };
154
+ }
155
+ writePiConfigFile(partial, path);
156
+ return { path, scope: options.scope };
157
+ }
158
+ export function parseAutoInstallValue(value) {
159
+ return !AUTO_INSTALL_DISABLED_VALUES.has(value.trim().toLowerCase());
160
+ }
161
+ function requireProjectDir(projectDir) {
162
+ if (!projectDir) {
163
+ throw new Error("projectDir is required when saving project config");
164
+ }
165
+ return projectDir;
166
+ }
167
+ function readStatusDisplayFromFile(configPath) {
168
+ const parsed = readPiConfigFile(configPath);
169
+ if (!parsed || typeof parsed[STATUS_DISPLAY_CONFIG_KEY] !== "string")
170
+ return undefined;
171
+ const value = parsed[STATUS_DISPLAY_CONFIG_KEY];
172
+ return isStatusDisplay(value) ? value : undefined;
173
+ }
174
+ function readEnvValueFromFile(configPath, key) {
175
+ const env = readPiConfigEnv(configPath);
176
+ return env[key];
177
+ }
178
+ function isStatusDisplay(value) {
179
+ return STATUS_DISPLAY_VALUES.includes(value);
180
+ }
181
+ function isRecord(value) {
182
+ return typeof value === "object" && value !== null && !Array.isArray(value);
183
+ }