pi-x-ide 1.3.0 → 1.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/src/pi/zed.ts CHANGED
@@ -6,6 +6,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent" with {
6
6
  "resolution-mode": "import",
7
7
  };
8
8
  import type { EditorSelectionSnapshot, SelectionRange } from "../shared/protocol";
9
+ import { resolvePiConfigEnv, isProcessEnvOrPiConfigOverlay } from "../shared/config";
9
10
  import { snapshotKey } from "../shared/format";
10
11
  import { isPathInsideOrEqual } from "../shared/paths";
11
12
  import { setLatestSelection, clearLatestSelection } from "./context";
@@ -18,12 +19,14 @@ export const ZED_POLL_INTERVAL_MS = 1000;
18
19
  // ── Terminal detection ──────────────────────────────────────────
19
20
 
20
21
  export function isZedTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
21
- return env.ZED_TERM === "true" || env.TERM_PROGRAM?.toLowerCase() === "zed";
22
+ const configuredEnv = resolvePiConfigEnv(env);
23
+ return configuredEnv.ZED_TERM === "true" || configuredEnv.TERM_PROGRAM?.toLowerCase() === "zed";
22
24
  }
23
25
 
24
26
  export function isWsl(env: NodeJS.ProcessEnv = process.env): boolean {
25
- if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
26
- if (env !== process.env) return false;
27
+ const configuredEnv = resolvePiConfigEnv(env);
28
+ if (configuredEnv.WSL_DISTRO_NAME || configuredEnv.WSL_INTEROP) return true;
29
+ if (!isProcessEnvOrPiConfigOverlay(configuredEnv)) return false;
27
30
  try {
28
31
  return /microsoft|wsl/i.test(readFileSync("/proc/version", "utf8"));
29
32
  } catch {
@@ -32,7 +35,8 @@ export function isWsl(env: NodeJS.ProcessEnv = process.env): boolean {
32
35
  }
33
36
 
34
37
  export function normalizeZedPathForHost(input: string, env: NodeJS.ProcessEnv = process.env): string {
35
- if (!input || !isWsl(env)) return input;
38
+ const configuredEnv = resolvePiConfigEnv(env);
39
+ if (!input || !isWsl(configuredEnv)) return input;
36
40
 
37
41
  const driveMatch = input.match(/^([a-zA-Z]):[\\/](.*)$/);
38
42
  if (driveMatch) {
@@ -45,7 +49,7 @@ export function normalizeZedPathForHost(input: string, env: NodeJS.ProcessEnv =
45
49
  if (uncMatch) {
46
50
  const distro = uncMatch[1];
47
51
  const rest = uncMatch[2].replaceAll("\\", "/");
48
- const currentDistro = env.WSL_DISTRO_NAME;
52
+ const currentDistro = configuredEnv.WSL_DISTRO_NAME;
49
53
  if (!currentDistro || distro.toLowerCase() === currentDistro.toLowerCase()) {
50
54
  return `/${rest}`;
51
55
  }
@@ -57,9 +61,10 @@ export function normalizeZedPathForHost(input: string, env: NodeJS.ProcessEnv =
57
61
  // ── DB path resolution ─────────────────────────────────────────
58
62
 
59
63
  export function resolveZedDbPath(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string | undefined {
60
- const override = env[PI_X_IDE_ZED_DB_ENV]?.trim();
64
+ const configuredEnv = resolvePiConfigEnv(env);
65
+ const override = configuredEnv[PI_X_IDE_ZED_DB_ENV]?.trim();
61
66
  if (override) {
62
- const normalizedOverride = normalizeZedPathForHost(override, env);
67
+ const normalizedOverride = normalizeZedPathForHost(override, configuredEnv);
63
68
  return isFile(normalizedOverride) ? normalizedOverride : undefined;
64
69
  }
65
70
 
@@ -67,8 +72,8 @@ export function resolveZedDbPath(env: NodeJS.ProcessEnv = process.env, home: str
67
72
  resolve(home, ".local", "share", "zed", "db", "0-stable", "db.sqlite"), // Linux
68
73
  resolve(home, "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), // macOS
69
74
  resolve(home, "AppData", "Local", "Zed", "db", "0-stable", "db.sqlite"), // Windows
70
- ...zedDbCandidatesFromWindowsEnv(env),
71
- ...zedDbCandidatesFromWslMount(env),
75
+ ...zedDbCandidatesFromWindowsEnv(configuredEnv),
76
+ ...zedDbCandidatesFromWslMount(configuredEnv),
72
77
  ];
73
78
 
74
79
  return candidates.find(isFile);
@@ -114,10 +119,11 @@ interface ZedDatabaseHandle {
114
119
  }
115
120
 
116
121
  function openZedDatabase(dbPath: string, env: NodeJS.ProcessEnv = process.env): ZedDatabaseHandle | undefined {
122
+ const configuredEnv = resolvePiConfigEnv(env);
117
123
  // Direct open on live WAL-mode databases can succeed at construction time
118
124
  // but fail on the first query on WSL/Windows mounts. Always snapshot
119
125
  // under WSL to avoid "disk I/O error" during SQL execution.
120
- if (isWsl(env)) return openZedDatabaseSnapshot(dbPath);
126
+ if (isWsl(configuredEnv)) return openZedDatabaseSnapshot(dbPath);
121
127
 
122
128
  try {
123
129
  return { db: new DatabaseSync(dbPath, { readOnly: true }), cleanup: () => undefined };
@@ -274,7 +280,8 @@ export interface ResolveZedSelectionOptions {
274
280
  }
275
281
 
276
282
  export function resolveZedSelection(options: ResolveZedSelectionOptions): EditorSelectionSnapshot | undefined {
277
- const { dbPath, cwd, readFile = (path) => readFileSync(path, "utf8"), env = process.env } = options;
283
+ const { dbPath, cwd, readFile = (path) => readFileSync(path, "utf8"), env: inputEnv = process.env } = options;
284
+ const env = resolvePiConfigEnv(inputEnv);
278
285
  const dbHandle = openZedDatabase(dbPath, env);
279
286
  if (!dbHandle) return undefined;
280
287
 
@@ -431,7 +438,7 @@ export function startZedPolling(
431
438
  env?: NodeJS.ProcessEnv;
432
439
  },
433
440
  ): boolean {
434
- const env = options?.env ?? process.env;
441
+ const env = resolvePiConfigEnv(options?.env ?? process.env);
435
442
  if (!isZedTerminal(env)) return false;
436
443
 
437
444
  const dbPath = options?.dbPath ?? resolveZedDbPath(env);
@@ -0,0 +1,100 @@
1
+ export type ConfigEnvValueType = "string" | "number" | "boolean";
2
+
3
+ export interface ConfigEnvOption {
4
+ readonly type: readonly ConfigEnvValueType[];
5
+ readonly description: string;
6
+ readonly default?: string;
7
+ }
8
+
9
+ export interface ConfigEnvPatternOption extends ConfigEnvOption {
10
+ readonly pattern: string;
11
+ }
12
+
13
+ export const CONFIG_ENV_VALUE_TYPES = ["string", "number", "boolean"] as const satisfies readonly ConfigEnvValueType[];
14
+
15
+ export function isConfigEnvValue(value: unknown): value is string | number | boolean {
16
+ return CONFIG_ENV_VALUE_TYPES.some((type) => typeof value === type);
17
+ }
18
+
19
+ export const CONFIG_ENV_OPTIONS = {
20
+ PI_X_IDE_LOCK_DIR: {
21
+ type: ["string"],
22
+ default: "~/.pi/pi-x-ide/lock",
23
+ description: "Directory containing Pi x IDE lock files. Defaults to ~/.pi/pi-x-ide/lock.",
24
+ },
25
+ PI_X_IDE_AUTO_INSTALL: {
26
+ type: ["string", "number", "boolean"],
27
+ default: "enabled",
28
+ description: "Controls VS Code-family extension auto-install. Values 0, false, and off disable it.",
29
+ },
30
+ PI_X_IDE_ZED_DB: {
31
+ type: ["string"],
32
+ description: "Override path to Zed's SQLite database.",
33
+ },
34
+ TERM_PROGRAM: {
35
+ type: ["string"],
36
+ description: "Terminal program marker used to detect VS Code, Cursor, Windsurf, or Zed.",
37
+ },
38
+ VSCODE_CWD: {
39
+ type: ["string"],
40
+ description: "VS Code-family cwd marker and IDE path hint.",
41
+ },
42
+ VSCODE_PID: {
43
+ type: ["string", "number"],
44
+ description: "VS Code-family process marker.",
45
+ },
46
+ VSCODE_IPC_HOOK_CLI: {
47
+ type: ["string"],
48
+ description: "VS Code-family IPC marker and IDE path hint.",
49
+ },
50
+ VSCODE_GIT_IPC_HANDLE: {
51
+ type: ["string"],
52
+ description: "VS Code-family Git IPC marker and IDE path hint.",
53
+ },
54
+ ZED_TERM: {
55
+ type: ["string", "boolean"],
56
+ description: "Zed terminal marker. Pi x IDE detects Zed when this is true.",
57
+ },
58
+ WSL_DISTRO_NAME: {
59
+ type: ["string"],
60
+ description: "WSL distribution name used for WSL path normalization and Zed database discovery.",
61
+ },
62
+ WSL_INTEROP: {
63
+ type: ["string"],
64
+ description: "WSL interop marker used for WSL path normalization and Zed database discovery.",
65
+ },
66
+ LOCALAPPDATA: {
67
+ type: ["string"],
68
+ description: "Windows local application data directory used to find Zed's database.",
69
+ },
70
+ USERPROFILE: {
71
+ type: ["string"],
72
+ description: "Windows user profile directory used to find Zed's database when LOCALAPPDATA is unavailable.",
73
+ },
74
+ PATH: {
75
+ type: ["string"],
76
+ description: "Executable search path used to find code, cursor, and windsurf CLIs.",
77
+ },
78
+ Path: {
79
+ type: ["string"],
80
+ description: "Windows-style executable search path used to find code, cursor, and windsurf CLIs.",
81
+ },
82
+ path: {
83
+ type: ["string"],
84
+ description: "Lowercase executable search path used to find code, cursor, and windsurf CLIs.",
85
+ },
86
+ PATHEXT: {
87
+ type: ["string"],
88
+ description: "Windows executable extensions used when searching for IDE CLIs.",
89
+ },
90
+ } as const satisfies Record<string, ConfigEnvOption>;
91
+
92
+ export const CONFIG_ENV_PATTERN_OPTIONS = [
93
+ {
94
+ pattern: "^(CURSOR|WINDSURF|CODEIUM).*",
95
+ type: ["string", "number", "boolean"],
96
+ description: "IDE-specific marker used to detect Cursor or Windsurf terminals.",
97
+ },
98
+ ] as const satisfies readonly ConfigEnvPatternOption[];
99
+
100
+ export type ConfigEnvOptionName = keyof typeof CONFIG_ENV_OPTIONS;
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { resolve } from "node:path";
4
+ import { isConfigEnvValue } from "./config-options";
5
+
6
+ export const PI_CONFIG_FILE = "config.json";
7
+
8
+ const processEnvOverlays = new WeakSet<NodeJS.ProcessEnv>();
9
+
10
+ export function resolvePiConfigPath(home: string = homedir()): string {
11
+ return resolve(home, ".pi", PI_CONFIG_FILE);
12
+ }
13
+
14
+ export function readPiConfigEnv(configPath: string = resolvePiConfigPath()): NodeJS.ProcessEnv {
15
+ let parsed: unknown;
16
+ try {
17
+ parsed = JSON.parse(readFileSync(configPath, "utf8")) as unknown;
18
+ } catch {
19
+ return {};
20
+ }
21
+
22
+ if (!isRecord(parsed) || !isRecord(parsed.env)) return {};
23
+
24
+ return Object.fromEntries(
25
+ Object.entries(parsed.env)
26
+ .filter((entry): entry is [string, string | number | boolean] => {
27
+ const [key, value] = entry;
28
+ return key.length > 0 && isConfigEnvValue(value);
29
+ })
30
+ .map(([key, value]) => [key, String(value)]),
31
+ );
32
+ }
33
+
34
+ export function resolvePiConfigEnv(
35
+ env: NodeJS.ProcessEnv = process.env,
36
+ options: { configPath?: string } = {},
37
+ ): NodeJS.ProcessEnv {
38
+ if (env !== process.env && !options.configPath) return env;
39
+
40
+ const configEnv = readPiConfigEnv(options.configPath);
41
+ if (Object.keys(configEnv).length === 0) return env;
42
+
43
+ const merged = { ...configEnv, ...env };
44
+ if (env === process.env) processEnvOverlays.add(merged);
45
+ return merged;
46
+ }
47
+
48
+ export function isProcessEnvOrPiConfigOverlay(env: NodeJS.ProcessEnv): boolean {
49
+ return env === process.env || processEnvOverlays.has(env);
50
+ }
51
+
52
+ function isRecord(value: unknown): value is Record<string, unknown> {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
@@ -1,11 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { resolvePiConfigEnv } from "./config";
3
4
  import { LOCK_DIR_ENV } from "./protocol";
4
5
 
5
6
  export function resolveLockDir(env: NodeJS.ProcessEnv = process.env): string {
6
- return env[LOCK_DIR_ENV] && env[LOCK_DIR_ENV].trim()
7
- ? resolve(env[LOCK_DIR_ENV])
8
- : resolve(homedir(), ".pi", "pi-x-ide");
7
+ const configuredEnv = resolvePiConfigEnv(env);
8
+ const override = configuredEnv[LOCK_DIR_ENV]?.trim();
9
+ return override ? resolve(override) : resolve(homedir(), ".pi", "pi-x-ide", "lock");
9
10
  }
10
11
 
11
12
  export function normalizePath(input: string): string {