@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,308 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access } from "node:fs/promises";
3
+ import { delimiter, isAbsolute, join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import * as Effect from "effect/Effect";
6
+ import { resolvePiConfigEnv } from "../shared/config.js";
7
+ import { InstallCommandError } from "../shared/effect-errors.js";
8
+ import { PI_X_IDE_VERSION } from "../shared/version.js";
9
+ const execFileAsync = promisify(execFile);
10
+ export const PI_X_IDE_EXTENSION_ID = "balaenis.pi-x-ide";
11
+ export const PI_X_IDE_AUTO_INSTALL_ENV = "PI_X_IDE_AUTO_INSTALL";
12
+ export const PI_X_IDE_TARGET_VERSION = PI_X_IDE_VERSION;
13
+ const DEFAULT_LIST_EXTENSIONS_TIMEOUT_MS = 15_000;
14
+ const DEFAULT_INSTALL_EXTENSION_TIMEOUT_MS = 60_000;
15
+ export const SUPPORTED_IDE_CLI_PROFILES = [
16
+ { id: "vscode", label: "VS Code", command: "code" },
17
+ { id: "cursor", label: "Cursor", command: "cursor" },
18
+ { id: "windsurf", label: "Windsurf", command: "windsurf" },
19
+ ];
20
+ export function isAutoInstallEnabled(env = process.env, options = {}) {
21
+ const configuredEnv = resolvePiConfigEnv(env, options);
22
+ const value = configuredEnv[PI_X_IDE_AUTO_INSTALL_ENV];
23
+ if (value === undefined)
24
+ return true;
25
+ return !["0", "false", "off"].includes(value.trim().toLowerCase());
26
+ }
27
+ export function parseInstalledExtensionVersion(output, extensionId = PI_X_IDE_EXTENSION_ID) {
28
+ const target = extensionId.toLowerCase();
29
+ for (const rawLine of output.split(/\r?\n/)) {
30
+ const line = rawLine.trim();
31
+ if (!line)
32
+ continue;
33
+ const separatorIndex = line.lastIndexOf("@");
34
+ if (separatorIndex <= 0)
35
+ continue;
36
+ const id = line.slice(0, separatorIndex).toLowerCase();
37
+ if (id === target)
38
+ return line.slice(separatorIndex + 1).trim() || undefined;
39
+ }
40
+ return undefined;
41
+ }
42
+ export function compareExtensionVersions(installed, target) {
43
+ if (!installed)
44
+ return "unknown";
45
+ const installedParts = parseStableVersion(installed);
46
+ const targetParts = parseStableVersion(target);
47
+ if (!installedParts || !targetParts)
48
+ return "unknown";
49
+ for (let index = 0; index < 3; index += 1) {
50
+ if (installedParts[index] < targetParts[index])
51
+ return "older";
52
+ if (installedParts[index] > targetParts[index])
53
+ return "newer";
54
+ }
55
+ return "equal";
56
+ }
57
+ export function inferCurrentIdeFromEnv(env = process.env) {
58
+ const configuredEnv = resolvePiConfigEnv(env);
59
+ const matches = new Set();
60
+ const hasWindsurfMarker = hasWindsurfEnvMarker(configuredEnv);
61
+ const hasCursorMarker = hasCursorEnvMarker(configuredEnv);
62
+ const hasVscodeMarker = hasVscodeEnvMarker(configuredEnv);
63
+ if (hasWindsurfMarker)
64
+ matches.add("windsurf");
65
+ if (hasCursorMarker)
66
+ matches.add("cursor");
67
+ if (hasVscodeMarker && !hasWindsurfMarker && !hasCursorMarker)
68
+ matches.add("vscode");
69
+ return matches.size === 1 ? [...matches][0] : undefined;
70
+ }
71
+ function hasWindsurfEnvMarker(env) {
72
+ if (env.TERM_PROGRAM?.toLowerCase() === "windsurf")
73
+ return true;
74
+ if (hasEnvKey(env, [/^WINDSURF/, /^CODEIUM/]))
75
+ return true;
76
+ return idePathHints(env).some((value) => envValueMatches(value, [/WINDSURF/, /CODEIUM/]));
77
+ }
78
+ function hasCursorEnvMarker(env) {
79
+ if (env.TERM_PROGRAM?.toLowerCase() === "cursor")
80
+ return true;
81
+ if (hasEnvKey(env, [/^CURSOR/]))
82
+ return true;
83
+ return idePathHints(env).some((value) => envValueMatches(value, [/CURSOR/]));
84
+ }
85
+ function hasVscodeEnvMarker(env) {
86
+ if (env.TERM_PROGRAM?.toLowerCase() === "vscode")
87
+ return true;
88
+ return Boolean(env.VSCODE_CWD || env.VSCODE_PID || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE);
89
+ }
90
+ function hasEnvKey(env, patterns) {
91
+ return Object.keys(env).some((key) => patterns.some((pattern) => pattern.test(key.toUpperCase())));
92
+ }
93
+ function idePathHints(env) {
94
+ return [env.VSCODE_CWD, env.VSCODE_IPC_HOOK_CLI, env.VSCODE_GIT_IPC_HANDLE];
95
+ }
96
+ function envValueMatches(value, patterns) {
97
+ const upperValue = value?.toUpperCase();
98
+ return upperValue ? patterns.some((pattern) => pattern.test(upperValue)) : false;
99
+ }
100
+ export function buildInstallArgs() {
101
+ return ["--force", "--install-extension", PI_X_IDE_EXTENSION_ID];
102
+ }
103
+ export async function findExecutable(command, env = process.env) {
104
+ const configuredEnv = resolvePiConfigEnv(env);
105
+ const pathEnv = configuredEnv.PATH ?? configuredEnv.Path ?? configuredEnv.path;
106
+ if (!pathEnv)
107
+ return undefined;
108
+ const extensions = process.platform === "win32" ? parsePathExt(configuredEnv) : [""];
109
+ const candidates = isAbsolute(command)
110
+ ? [command]
111
+ : pathEnv
112
+ .split(delimiter)
113
+ .filter(Boolean)
114
+ .flatMap((directory) => extensions.map((extension) => join(directory, withExtension(command, extension))));
115
+ for (const candidate of candidates) {
116
+ try {
117
+ await access(candidate);
118
+ return candidate;
119
+ }
120
+ catch {
121
+ }
122
+ }
123
+ return undefined;
124
+ }
125
+ export async function runCli(cliPath, args, timeoutMs = DEFAULT_LIST_EXTENSIONS_TIMEOUT_MS) {
126
+ if (process.platform === "win32" && isCmdOrBatFile(cliPath)) {
127
+ const { stdout, stderr } = await execFileAsync(resolveCmdExe(), ["/d", "/c", cliPath, ...args], {
128
+ timeout: timeoutMs,
129
+ windowsHide: true,
130
+ maxBuffer: 1024 * 1024,
131
+ });
132
+ return { stdout: String(stdout), stderr: String(stderr) };
133
+ }
134
+ const { stdout, stderr } = await execFileAsync(cliPath, args, {
135
+ timeout: timeoutMs,
136
+ windowsHide: true,
137
+ maxBuffer: 1024 * 1024,
138
+ });
139
+ return { stdout: String(stdout), stderr: String(stderr) };
140
+ }
141
+ function listExtensionVersion(cliPath, timeoutMs) {
142
+ return Effect.promise(async () => {
143
+ try {
144
+ const { stdout } = await runCli(cliPath, ["--list-extensions", "--show-versions"], timeoutMs);
145
+ return { installedVersion: parseInstalledExtensionVersion(stdout) };
146
+ }
147
+ catch (error) {
148
+ return { listError: error instanceof Error ? error.message : String(error) };
149
+ }
150
+ });
151
+ }
152
+ export function discoverInstallCandidatesEffect(options = {}) {
153
+ return Effect.gen(function* () {
154
+ const env = resolvePiConfigEnv(options.env ?? process.env);
155
+ const currentIde = inferCurrentIdeFromEnv(env);
156
+ const includeLowConfidence = options.includeLowConfidence ?? false;
157
+ const timeoutMs = options.timeoutMs ?? DEFAULT_LIST_EXTENSIONS_TIMEOUT_MS;
158
+ const candidates = [];
159
+ for (const profile of SUPPORTED_IDE_CLI_PROFILES) {
160
+ const cliPath = yield* Effect.promise(() => findExecutable(profile.command, env));
161
+ if (!cliPath)
162
+ continue;
163
+ const confidence = currentIde === profile.id ? "current-terminal" : "available-cli";
164
+ if (confidence !== "current-terminal" && !includeLowConfidence)
165
+ continue;
166
+ const { installedVersion, listError } = yield* listExtensionVersion(cliPath, timeoutMs);
167
+ const reason = resolveInstallReason(installedVersion, PI_X_IDE_TARGET_VERSION, listError);
168
+ candidates.push({
169
+ id: profile.id,
170
+ label: profile.label,
171
+ cli: profile.command,
172
+ cliPath,
173
+ confidence,
174
+ installedVersion,
175
+ targetVersion: PI_X_IDE_TARGET_VERSION,
176
+ needsInstall: reason === "missing" || reason === "outdated" || reason === "unknown",
177
+ reason,
178
+ listError,
179
+ });
180
+ }
181
+ return candidates;
182
+ });
183
+ }
184
+ export async function discoverInstallCandidates(options = {}) {
185
+ return Effect.runPromise(discoverInstallCandidatesEffect(options));
186
+ }
187
+ export function selectAutoInstallCandidate(candidates, env = process.env) {
188
+ const configuredEnv = resolvePiConfigEnv(env);
189
+ const currentIde = inferCurrentIdeFromEnv(configuredEnv);
190
+ const highConfidence = candidates.filter((candidate) => candidate.confidence === "current-terminal" &&
191
+ candidate.reason !== "unknown" &&
192
+ (!currentIde || candidate.id === currentIde));
193
+ return highConfidence.length === 1 ? highConfidence[0] : undefined;
194
+ }
195
+ export function installIdeExtensionEffect(candidate, runtime, options = {}) {
196
+ return Effect.gen(function* () {
197
+ if (!candidate.needsInstall) {
198
+ return { candidate, skipped: true, success: true, stdout: "", stderr: "" };
199
+ }
200
+ const installKey = candidate.id;
201
+ if (runtime.installingIdeIds.has(installKey)) {
202
+ return {
203
+ candidate,
204
+ skipped: true,
205
+ success: false,
206
+ stdout: "",
207
+ stderr: "",
208
+ error: `${candidate.label} extension install is already in progress.`,
209
+ };
210
+ }
211
+ runtime.installingIdeIds.add(installKey);
212
+ return yield* Effect.promise(async () => {
213
+ try {
214
+ const { stdout, stderr } = await runCli(candidate.cliPath, buildInstallArgs(), options.timeoutMs ?? DEFAULT_INSTALL_EXTENSION_TIMEOUT_MS);
215
+ return { candidate, skipped: false, success: true, stdout, stderr };
216
+ }
217
+ catch (error) {
218
+ const stdout = getExecOutput(error, "stdout");
219
+ const stderr = getExecOutput(error, "stderr");
220
+ const tagged = new InstallCommandError({
221
+ cli: candidate.cli,
222
+ code: getExecExitCode(error),
223
+ stderr,
224
+ stdout,
225
+ });
226
+ return {
227
+ candidate,
228
+ skipped: false,
229
+ success: false,
230
+ stdout: tagged.stdout ?? "",
231
+ stderr: tagged.stderr,
232
+ error: error instanceof Error ? error.message : String(error),
233
+ };
234
+ }
235
+ finally {
236
+ runtime.installingIdeIds.delete(installKey);
237
+ }
238
+ });
239
+ });
240
+ }
241
+ export async function installIdeExtension(candidate, runtime, options = {}) {
242
+ return Effect.runPromise(installIdeExtensionEffect(candidate, runtime, options));
243
+ }
244
+ function resolveInstallReason(installedVersion, targetVersion, listError) {
245
+ if (listError)
246
+ return "unknown";
247
+ if (!installedVersion)
248
+ return "missing";
249
+ const comparison = compareExtensionVersions(installedVersion, targetVersion);
250
+ if (comparison === "older")
251
+ return "outdated";
252
+ if (comparison === "equal" || comparison === "newer")
253
+ return "current";
254
+ return "unknown";
255
+ }
256
+ function parseStableVersion(version) {
257
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/.exec(version.trim());
258
+ if (!match)
259
+ return undefined;
260
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
261
+ }
262
+ function parsePathExt(env) {
263
+ const pathExt = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
264
+ const values = pathExt
265
+ .split(";")
266
+ .map((extension) => extension.trim())
267
+ .filter(Boolean);
268
+ if (process.platform !== "win32") {
269
+ return values.length > 0 ? ["", ...values] : [""];
270
+ }
271
+ return values;
272
+ }
273
+ function withExtension(command, extension) {
274
+ if (!extension)
275
+ return command;
276
+ return command.toLowerCase().endsWith(extension.toLowerCase()) ? command : `${command}${extension}`;
277
+ }
278
+ function isCmdOrBatFile(cliPath) {
279
+ const lowerPath = cliPath.toLowerCase();
280
+ return lowerPath.endsWith(".cmd") || lowerPath.endsWith(".bat");
281
+ }
282
+ function resolveCmdExe() {
283
+ return process.env.ComSpec ?? "cmd.exe";
284
+ }
285
+ function getExecOutput(error, key) {
286
+ if (typeof error !== "object" || error === null || !(key in error))
287
+ return "";
288
+ const value = error[key];
289
+ if (typeof value === "string")
290
+ return value;
291
+ if (Buffer.isBuffer(value))
292
+ return value.toString("utf8");
293
+ if (value === undefined || value === null)
294
+ return "";
295
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
296
+ return value.toString();
297
+ return "";
298
+ }
299
+ function getExecExitCode(error) {
300
+ if (typeof error !== "object" || error === null || !("code" in error))
301
+ return undefined;
302
+ const value = error.code;
303
+ if (typeof value === "number" && Number.isFinite(value))
304
+ return value;
305
+ if (value === null)
306
+ return null;
307
+ return undefined;
308
+ }
@@ -0,0 +1,57 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Fiber from "effect/Fiber";
3
+ import { updateIdeUi } from "./ui.js";
4
+ export const MAX_RECONNECT_ATTEMPTS = 3;
5
+ export const RECONNECT_DELAY_MS = 2_000;
6
+ export function resetReconnectState(runtime) {
7
+ runtime.reconnectAttempts = 0;
8
+ runtime.reconnectCandidateKey = undefined;
9
+ }
10
+ export function recordReconnectAttempt(runtime, candidate) {
11
+ const key = candidate ? reconnectCandidateKey(candidate) : undefined;
12
+ if (runtime.reconnectCandidateKey !== key) {
13
+ runtime.reconnectCandidateKey = key;
14
+ runtime.reconnectAttempts = 0;
15
+ }
16
+ if (runtime.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS)
17
+ return undefined;
18
+ runtime.reconnectAttempts += 1;
19
+ return runtime.reconnectAttempts;
20
+ }
21
+ export function formatReconnectLimitMessage(candidate) {
22
+ const target = candidate ? `${candidate.lock.name} at ${candidate.lock.host}:${candidate.lock.port}` : "IDE";
23
+ return `Stopped reconnecting to ${target} after ${MAX_RECONNECT_ATTEMPTS} attempts.`;
24
+ }
25
+ export function stopReconnectScheduling(runtime) {
26
+ const fiber = runtime.reconnectFiber;
27
+ runtime.reconnectFiber = undefined;
28
+ if (fiber)
29
+ Effect.runFork(Fiber.interrupt(fiber));
30
+ }
31
+ export function scheduleReconnect(runtime, reconnect) {
32
+ if (runtime.reconnectFiber || !runtime.enabled)
33
+ return;
34
+ const generation = runtime.sessionGeneration;
35
+ const attempt = recordReconnectAttempt(runtime, runtime.currentCandidate);
36
+ if (attempt === undefined) {
37
+ runtime.connectionStatus = "error";
38
+ runtime.connectionMessage = formatReconnectLimitMessage(runtime.currentCandidate);
39
+ updateIdeUi(runtime);
40
+ return;
41
+ }
42
+ const fiber = Effect.runFork(Effect.gen(function* () {
43
+ yield* Effect.sleep(`${RECONNECT_DELAY_MS} millis`);
44
+ if (runtime.reconnectFiber === fiber)
45
+ runtime.reconnectFiber = undefined;
46
+ if (!runtime.enabled || runtime.sessionGeneration !== generation)
47
+ return;
48
+ yield* Effect.promise(() => reconnect(generation));
49
+ }).pipe(Effect.ensuring(Effect.sync(() => {
50
+ if (runtime.reconnectFiber === fiber)
51
+ runtime.reconnectFiber = undefined;
52
+ }))));
53
+ runtime.reconnectFiber = fiber;
54
+ }
55
+ function reconnectCandidateKey(candidate) {
56
+ return `${candidate.path}:${candidate.lock.host}:${candidate.lock.port}:${candidate.mtimeMs}`;
57
+ }
@@ -0,0 +1,38 @@
1
+ const defaultImporter = () => import("./runtime-services.js");
2
+ let importer = defaultImporter;
3
+ let cachedPromise;
4
+ export function preloadRuntimeServices() {
5
+ return loadRuntimeServices();
6
+ }
7
+ export function loadRuntimeServices() {
8
+ if (!cachedPromise) {
9
+ cachedPromise = importer().then((module) => module, (error) => {
10
+ cachedPromise = undefined;
11
+ throw error;
12
+ });
13
+ }
14
+ return cachedPromise;
15
+ }
16
+ export function createRuntimeServicesLoaderForTests(nextImporter = defaultImporter) {
17
+ let testCached;
18
+ const load = () => {
19
+ if (!testCached) {
20
+ testCached = nextImporter().then((module) => module, (error) => {
21
+ testCached = undefined;
22
+ throw error;
23
+ });
24
+ }
25
+ return testCached;
26
+ };
27
+ return {
28
+ preloadRuntimeServices: load,
29
+ loadRuntimeServices: load,
30
+ reset: () => {
31
+ testCached = undefined;
32
+ },
33
+ };
34
+ }
35
+ export function setRuntimeServicesImporterForTests(nextImporter) {
36
+ importer = nextImporter ?? defaultImporter;
37
+ cachedPromise = undefined;
38
+ }