parallel-codex-tui 0.1.3 → 0.1.5

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 (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
package/dist/doctor.js CHANGED
@@ -1,11 +1,23 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { constants } from "node:fs";
2
3
  import { access } from "node:fs/promises";
4
+ import { createConnection } from "node:net";
3
5
  import { delimiter, join } from "node:path";
4
6
  import { prepareAppRoot } from "./core/app-root.js";
5
- import { configPath, loadConfig } from "./core/config.js";
6
- import { pathExists } from "./core/file-store.js";
7
+ import { formatConfigErrorMessage } from "./core/config-errors.js";
8
+ import { configPath, loadConfig, withUiThemeOverride } from "./core/config.js";
9
+ import { ensureDir, pathExists } from "./core/file-store.js";
10
+ import { routerRuntimeDir } from "./core/paths.js";
11
+ import { diagnoseRouterFailure } from "./core/router-audit.js";
12
+ import { routeRequestWithCodex, routerProxyConfigured } from "./core/router.js";
7
13
  import { prepareWorkspace } from "./core/workspace.js";
8
- export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
14
+ import { auditTuiThemeContrast, TUI_THEME_MIN_CONTRAST_RATIO } from "./tui/theme-contrast.js";
15
+ import { formatTuiThemePreview } from "./tui/theme-preview.js";
16
+ import { resolveTuiTheme } from "./tui/theme.js";
17
+ import { diagnoseAgentCapabilities } from "./workers/capabilities.js";
18
+ import { runLiveAgentProbes } from "./workers/live-probe.js";
19
+ import { workerProvider } from "./workers/provider.js";
20
+ export async function runDoctor(appRoot, workspaceRoot, env = process.env, options = {}) {
9
21
  const lines = ["parallel-codex-tui doctor"];
10
22
  let ok = true;
11
23
  await prepareAppRoot(appRoot);
@@ -15,7 +27,7 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
15
27
  }
16
28
  else {
17
29
  ok = false;
18
- lines.push(`Node.js: unsupported (${process.versions.node}; need 26+)`);
30
+ lines.push(`Node.js: unsupported (${process.versions.node}; need 24.15+)`);
19
31
  }
20
32
  lines.push(`workspace: ok (${preparedWorkspace})`);
21
33
  const localConfigPath = configPath(appRoot);
@@ -29,19 +41,78 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
29
41
  }
30
42
  let config;
31
43
  try {
32
- config = await loadConfig(appRoot);
44
+ const loadedConfig = await loadConfig(appRoot);
45
+ config = withUiThemeOverride(loadedConfig, options.theme ?? null);
33
46
  lines.push(`config: ok (${localConfigPath})`);
47
+ lines.push(`theme: ok (${themeSummary(config.ui.theme, loadedConfig.ui.theme, options.theme ?? null)}; ${themeOverrideSummary(config.ui.colors)})`);
48
+ const theme = resolveTuiTheme({ theme: config.ui.theme, colors: config.ui.colors });
49
+ lines.push(`palette: ${themePaletteSummary(theme)}`);
50
+ lines.push(...formatTuiThemePreview(theme));
51
+ lines.push(...formatThemeContrastAudit(auditTuiThemeContrast(theme)));
34
52
  }
35
53
  catch (error) {
36
54
  ok = false;
37
- lines.push(`config: invalid (${localConfigPath}; ${error.message})`);
55
+ lines.push(`config: invalid (${localConfigPath})`);
56
+ lines.push(...formatConfigErrorMessage(error).split("\n").map((line) => `config error: ${line}`));
38
57
  return {
39
58
  ok,
40
59
  text: `${lines.join("\n")}\n`
41
60
  };
42
61
  }
43
- for (const command of configuredCommands(config)) {
62
+ const includeRouter = config.router.defaultMode === "auto" || options.probeRouter === true;
63
+ if (includeRouter) {
64
+ lines.push(`router retry: ${config.router.codex.maxAttempts} attempts; transient only; ${config.router.codex.retryDelayMs}ms backoff (TUI routing; live probe runs once)`);
65
+ lines.push(`router budget: total ${config.router.codex.timeoutMs}ms; follow-up ${config.router.codex.followUpTimeoutMs}ms; first output ${config.router.codex.firstOutputTimeoutMs}ms; idle ${config.router.codex.idleTimeoutMs}ms`);
66
+ }
67
+ const preflight = await runRuntimePreflight(config, preparedWorkspace, env, {
68
+ includeRouter,
69
+ ...(options.capabilityRunner ? { capabilityRunner: options.capabilityRunner } : {}),
70
+ ...(options.capabilityTimeoutMs ? { capabilityTimeoutMs: options.capabilityTimeoutMs } : {})
71
+ });
72
+ lines.push(...preflight.lines);
73
+ ok = ok && preflight.ok;
74
+ if (options.probeAgents) {
75
+ if (ok) {
76
+ const liveAgents = await runLiveAgentProbes(config, preparedWorkspace, configuredWorkerEngines(config), options.liveAgentProbeOptions);
77
+ lines.push(...liveAgents.lines);
78
+ ok = ok && liveAgents.ok;
79
+ }
80
+ else {
81
+ lines.push("agent live probe: skipped (preflight failed)");
82
+ }
83
+ }
84
+ else {
85
+ lines.push("agent live probe: not run (add --probe-agents; may use model quota)");
86
+ }
87
+ if (options.probeRouter) {
88
+ const probe = await runRouterProbe(config, appRoot, options.routeRunner);
89
+ lines.push(probe.line);
90
+ ok = ok && probe.ok;
91
+ }
92
+ else if (config.router.defaultMode === "auto") {
93
+ lines.push("router live probe: not run (add --probe-router)");
94
+ }
95
+ return {
96
+ ok,
97
+ text: `${lines.join("\n")}\n`
98
+ };
99
+ }
100
+ export async function runRuntimePreflight(config, workspaceRoot, env = process.env, options = {}) {
101
+ const includeRouter = options.includeRouter ?? config.router.defaultMode === "auto";
102
+ const lines = [];
103
+ let ok = true;
104
+ try {
105
+ await access(workspaceRoot, constants.R_OK | constants.W_OK | constants.X_OK);
106
+ lines.push("workspace permissions: ok (read/write/search)");
107
+ }
108
+ catch {
109
+ ok = false;
110
+ lines.push(`workspace permissions: denied (${workspaceRoot}; need read/write/search)`);
111
+ }
112
+ const availableCommands = new Set();
113
+ for (const command of configuredCommands(config, includeRouter)) {
44
114
  if (await commandExists(command, env)) {
115
+ availableCommands.add(command);
45
116
  lines.push(`${command}: ok`);
46
117
  }
47
118
  else {
@@ -49,7 +120,7 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
49
120
  lines.push(`${command}: missing`);
50
121
  }
51
122
  }
52
- for (const check of configuredWorkerModelEnvChecks(config, env)) {
123
+ for (const check of configuredEnvironmentChecks(config, env, includeRouter)) {
53
124
  if (check.ok) {
54
125
  lines.push(`${check.label}: ok`);
55
126
  }
@@ -58,39 +129,204 @@ export async function runDoctor(appRoot, workspaceRoot, env = process.env) {
58
129
  lines.push(`${check.label}: missing env ${check.envName}`);
59
130
  }
60
131
  }
61
- return {
62
- ok,
63
- text: `${lines.join("\n")}\n`
132
+ const capabilityDiagnostics = await diagnoseAgentCapabilities(config, env, {
133
+ includeRouter,
134
+ workerEngines: configuredWorkerEngines(config),
135
+ availableCommands,
136
+ ...(options.capabilityRunner ? { runner: options.capabilityRunner } : {}),
137
+ ...(options.capabilityTimeoutMs ? { timeoutMs: options.capabilityTimeoutMs } : {})
138
+ });
139
+ lines.push(...capabilityDiagnostics.lines);
140
+ ok = ok && capabilityDiagnostics.ok;
141
+ const systemProxy = options.systemProxy !== undefined
142
+ ? options.systemProxy
143
+ : await detectMacSystemProxy();
144
+ const proxyDiagnostics = await diagnoseProxyEnvironment(config, env, systemProxy, options.proxyConnector ?? canConnectProxy, { includeRouter });
145
+ lines.push(...proxyDiagnostics.lines);
146
+ ok = ok && proxyDiagnostics.ok;
147
+ return { ok, lines };
148
+ }
149
+ export async function diagnoseProxyEnvironment(config, env, systemProxy, connect = canConnectProxy, options = {}) {
150
+ const contexts = [];
151
+ if (options.includeRouter ?? config.router.defaultMode === "auto") {
152
+ contexts.push({ label: "router proxy", env: config.router.codex.env, table: "router.codex.env" });
153
+ }
154
+ for (const engine of configuredWorkerEngines(config)) {
155
+ contexts.push({
156
+ label: `workers.${engine} proxy`,
157
+ env: workerProvider(config, engine).config.model.env,
158
+ table: `workers.${engine}.model.env`
159
+ });
160
+ }
161
+ const connectionCache = new Map();
162
+ const lines = [];
163
+ let ok = true;
164
+ for (const context of contexts) {
165
+ const endpoint = configuredProxyEndpoint(context.env, env);
166
+ if (!endpoint) {
167
+ if (systemProxy) {
168
+ lines.push(`${context.label}: warning (macOS system proxy ${formatProxyEndpoint(systemProxy)} is not inherited; configure [${context.table}])`);
169
+ }
170
+ else {
171
+ lines.push(`${context.label}: direct (no proxy configured)`);
172
+ }
173
+ continue;
174
+ }
175
+ if (endpoint === "invalid") {
176
+ ok = false;
177
+ lines.push(`${context.label}: invalid proxy URL`);
178
+ continue;
179
+ }
180
+ const key = formatProxyEndpoint(endpoint);
181
+ const reachable = await cachedProxyConnection(connectionCache, key, () => connect(endpoint.host, endpoint.port));
182
+ if (reachable) {
183
+ lines.push(`${context.label}: reachable (${key}; local endpoint only)`);
184
+ }
185
+ else {
186
+ ok = false;
187
+ lines.push(`${context.label}: unreachable (${key})`);
188
+ }
189
+ }
190
+ return { ok, lines };
191
+ }
192
+ async function runRouterProbe(config, appRoot, runner) {
193
+ const cwd = routerRuntimeDir(appRoot, config.dataDir);
194
+ await ensureDir(cwd);
195
+ const probeConfig = {
196
+ ...config,
197
+ router: {
198
+ ...config.router,
199
+ defaultMode: "auto"
200
+ }
64
201
  };
202
+ try {
203
+ const route = await routeRequestWithCodex("hello", probeConfig, runner, cwd);
204
+ if (route.source === "codex") {
205
+ const trace = formatRouterProbeTrace(route, false);
206
+ return {
207
+ ok: true,
208
+ line: `router live probe: ok (${route.mode} in ${Math.round(route.duration_ms ?? 0)}ms${trace ? `; ${trace}` : ""})`
209
+ };
210
+ }
211
+ const trace = formatRouterProbeTrace(route, true);
212
+ const diagnosis = diagnoseRouterFailure({
213
+ ...route,
214
+ reason: route.reason,
215
+ proxy_configured: routerProxyConfigured(config.router.codex.env)
216
+ });
217
+ return {
218
+ ok: false,
219
+ line: `router live probe: failed (${sanitizeDiagnosticText(route.reason)}${trace ? `; ${trace}` : ""}; diagnosis ${diagnosis.summary}; next ${diagnosis.action})`
220
+ };
221
+ }
222
+ catch (error) {
223
+ const message = error instanceof Error ? error.message : String(error);
224
+ return {
225
+ ok: false,
226
+ line: `router live probe: failed (${sanitizeDiagnosticText(message)})`
227
+ };
228
+ }
229
+ }
230
+ function formatRouterProbeTrace(route, includeStage) {
231
+ return [
232
+ ...(includeStage && route.router_failure_stage ? [`stage ${route.router_failure_stage}`] : []),
233
+ ...(includeStage && route.router_timeout_kind ? [`timeout ${route.router_timeout_kind}`] : []),
234
+ ...(typeof route.router_dispatch_ms === "number" ? [`dispatch ${Math.round(route.router_dispatch_ms)}ms`] : []),
235
+ ...(typeof route.router_spawn_ms === "number" ? [`spawn ${Math.round(route.router_spawn_ms)}ms`] : []),
236
+ ...formatRouterProbeFirstOutput(route),
237
+ ...(typeof route.router_process_ms === "number" ? [`process ${Math.round(route.router_process_ms)}ms`] : []),
238
+ ...(typeof route.router_parse_ms === "number" ? [`parse ${Math.round(route.router_parse_ms)}ms`] : []),
239
+ ...(typeof route.duration_ms === "number" ? [`total ${Math.round(route.duration_ms)}ms`] : []),
240
+ ...(typeof route.router_stdout_bytes === "number" ? [`stdout ${formatRouterProbeBytes(route.router_stdout_bytes)}`] : []),
241
+ ...(typeof route.router_stderr_bytes === "number" ? [`stderr ${formatRouterProbeBytes(route.router_stderr_bytes)}`] : [])
242
+ ].join("; ");
243
+ }
244
+ function formatRouterProbeFirstOutput(route) {
245
+ const streams = [
246
+ ...(typeof route.router_first_stdout_ms === "number"
247
+ ? [{ at: route.router_first_stdout_ms, text: `first stdout ${Math.round(route.router_first_stdout_ms)}ms` }]
248
+ : []),
249
+ ...(typeof route.router_first_stderr_ms === "number"
250
+ ? [{ at: route.router_first_stderr_ms, text: `first stderr ${Math.round(route.router_first_stderr_ms)}ms` }]
251
+ : [])
252
+ ];
253
+ if (streams.length > 0) {
254
+ return streams.sort((left, right) => left.at - right.at).map((stream) => stream.text);
255
+ }
256
+ if (typeof route.router_first_output_ms === "number") {
257
+ return [`first output ${Math.round(route.router_first_output_ms)}ms`];
258
+ }
259
+ return route.router_failure_stage === "waiting-output" ? ["first output none"] : [];
260
+ }
261
+ function formatRouterProbeBytes(bytes) {
262
+ if (bytes < 1024) {
263
+ return `${Math.round(bytes)}B`;
264
+ }
265
+ return `${(bytes / 1024).toFixed(bytes < 10 * 1024 ? 1 : 0)}KB`;
266
+ }
267
+ function sanitizeDiagnosticText(value) {
268
+ return value
269
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
270
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
271
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
272
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)([^@\s/]+)@/gi, "$1***@")
273
+ .replace(/\b(api[-_\s]?key|token|authorization)(\s*[:=]\s*)(\S+)/gi, "$1$2***")
274
+ .replace(/\s+/g, " ")
275
+ .trim()
276
+ .slice(0, 400) || "unknown error";
65
277
  }
66
278
  export function isSupportedNodeVersion(version) {
67
279
  const [majorRaw = "0", minorRaw = "0"] = version.split(".");
68
280
  const major = Number.parseInt(majorRaw, 10);
69
281
  const minor = Number.parseInt(minorRaw, 10);
70
- return major >= 26;
282
+ return major > 24 || (major === 24 && minor >= 15);
71
283
  }
72
- function configuredCommands(config) {
73
- return Array.from(new Set(configuredEngines(config, { includeRouter: true })
74
- .map((engine) => commandForEngine(config, engine))
75
- .filter((command) => Boolean(command) && command !== "mock")));
284
+ function configuredCommands(config, includeRouter) {
285
+ const commands = includeRouter ? [config.router.codex.command] : [];
286
+ for (const engine of configuredWorkerEngines(config)) {
287
+ const worker = config.workers[engine];
288
+ commands.push(worker.command);
289
+ if (worker.nativeSession.enabled) {
290
+ commands.push(worker.interactive.command);
291
+ }
292
+ }
293
+ return [...new Set(commands.filter((command) => command && command !== "mock"))];
76
294
  }
77
- function commandForEngine(config, engine) {
78
- if (engine === "router-codex") {
79
- return config.router.codex.command;
295
+ function themeOverrideSummary(colors) {
296
+ const entries = Object.entries(colors).sort(([left], [right]) => left.localeCompare(right));
297
+ if (entries.length === 0) {
298
+ return "no color overrides";
80
299
  }
81
- if (engine === "codex") {
82
- return config.workers.codex.command;
300
+ return `colors: ${entries.map(([key, value]) => `${key}=${value}`).join(", ")}`;
301
+ }
302
+ function themePaletteSummary(theme) {
303
+ return [
304
+ `chrome=${theme.chrome}`,
305
+ `surface=${theme.surface}`,
306
+ `rail=${theme.rail}`,
307
+ `accent=${theme.accent}`
308
+ ].join(", ");
309
+ }
310
+ function formatThemeContrastAudit(audit) {
311
+ if (audit.issues.length === 0) {
312
+ return [`theme contrast: ok (minimum ${formatContrastRatio(audit.minimumRatio)}:1 across ${audit.measurements.length} rendered pairs)`];
83
313
  }
84
- if (engine === "claude") {
85
- return config.workers.claude.command;
314
+ return [
315
+ `theme contrast: warning (${audit.issues.length} of ${audit.measurements.length} rendered pairs below ${TUI_THEME_MIN_CONTRAST_RATIO}:1)`,
316
+ ...audit.issues.map(({ foreground, background, ratio }) => `theme contrast issue: ${foreground}/${background} ${formatContrastRatio(ratio)}:1`)
317
+ ];
318
+ }
319
+ function formatContrastRatio(ratio) {
320
+ return ratio.toFixed(2);
321
+ }
322
+ function themeSummary(effectiveTheme, configTheme, cliTheme) {
323
+ if (cliTheme && cliTheme !== configTheme) {
324
+ return `${effectiveTheme} via --theme; config ${configTheme}`;
86
325
  }
87
- return null;
326
+ return effectiveTheme;
88
327
  }
89
- function configuredEngines(config, options) {
328
+ function configuredWorkerEngines(config) {
90
329
  const engines = new Set();
91
- if (config.router.defaultMode === "auto" && options.includeRouter) {
92
- engines.add("router-codex");
93
- }
94
330
  if (config.router.defaultMode === "auto") {
95
331
  engines.add(config.pairing.main);
96
332
  engines.add(config.pairing.judge);
@@ -105,15 +341,23 @@ function configuredEngines(config, options) {
105
341
  engines.add(config.pairing.actor);
106
342
  engines.add(config.pairing.critic);
107
343
  }
108
- return Array.from(engines);
344
+ return [...engines].filter((engine) => engine !== "mock");
109
345
  }
110
- function configuredWorkerEngines(config) {
111
- return configuredEngines(config, { includeRouter: false }).filter((engine) => engine === "codex" || engine === "claude");
112
- }
113
- function configuredWorkerModelEnvChecks(config, env) {
346
+ function configuredEnvironmentChecks(config, env, includeRouter) {
114
347
  const checks = [];
348
+ if (includeRouter) {
349
+ for (const [key, value] of Object.entries(config.router.codex.env)) {
350
+ for (const envName of referencedEnvNames(value)) {
351
+ checks.push({
352
+ label: `router.codex.env.${key}`,
353
+ envName,
354
+ ok: Boolean(env[envName])
355
+ });
356
+ }
357
+ }
358
+ }
115
359
  for (const engine of configuredWorkerEngines(config)) {
116
- const worker = engine === "codex" ? config.workers.codex : config.workers.claude;
360
+ const worker = workerProvider(config, engine).config;
117
361
  for (const [key, value] of Object.entries(worker.model.env)) {
118
362
  for (const envName of referencedEnvNames(value)) {
119
363
  checks.push({
@@ -153,3 +397,98 @@ async function canExecute(path) {
153
397
  return false;
154
398
  }
155
399
  }
400
+ function configuredProxyEndpoint(configured, processEnvironment) {
401
+ const effective = {
402
+ ...processEnvironment,
403
+ ...Object.fromEntries(Object.entries(configured).map(([name, value]) => [name, renderEnvironmentValue(value, processEnvironment)]))
404
+ };
405
+ const value = [
406
+ effective.HTTPS_PROXY,
407
+ effective.https_proxy,
408
+ effective.ALL_PROXY,
409
+ effective.all_proxy,
410
+ effective.HTTP_PROXY,
411
+ effective.http_proxy
412
+ ].find((candidate) => candidate?.trim())?.trim();
413
+ if (!value) {
414
+ return null;
415
+ }
416
+ try {
417
+ const url = new URL(value.includes("://") ? value : `http://${value}`);
418
+ const port = url.port ? Number(url.port) : defaultProxyPort(url.protocol);
419
+ if (!url.hostname || !Number.isInteger(port) || port <= 0 || port > 65535) {
420
+ return "invalid";
421
+ }
422
+ return { host: url.hostname, port };
423
+ }
424
+ catch {
425
+ return "invalid";
426
+ }
427
+ }
428
+ function renderEnvironmentValue(value, env) {
429
+ return value.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => env[name] ?? "");
430
+ }
431
+ function defaultProxyPort(protocol) {
432
+ if (protocol === "https:") {
433
+ return 443;
434
+ }
435
+ if (protocol.startsWith("socks")) {
436
+ return 1080;
437
+ }
438
+ return 80;
439
+ }
440
+ function formatProxyEndpoint(endpoint) {
441
+ const host = endpoint.host.includes(":") ? `[${endpoint.host}]` : endpoint.host;
442
+ return `${host}:${endpoint.port}`;
443
+ }
444
+ async function cachedProxyConnection(cache, key, connect) {
445
+ const existing = cache.get(key);
446
+ if (existing) {
447
+ return existing;
448
+ }
449
+ const pending = connect();
450
+ cache.set(key, pending);
451
+ return pending;
452
+ }
453
+ async function detectMacSystemProxy() {
454
+ if (process.platform !== "darwin") {
455
+ return null;
456
+ }
457
+ const output = await execFileText("scutil", ["--proxy"]);
458
+ if (!output) {
459
+ return null;
460
+ }
461
+ const https = parseScutilProxy(output, "HTTPS");
462
+ return https ?? parseScutilProxy(output, "HTTP");
463
+ }
464
+ function parseScutilProxy(output, prefix) {
465
+ const enabled = output.match(new RegExp(`${prefix}Enable\\s*:\\s*(\\d+)`))?.[1] === "1";
466
+ const host = output.match(new RegExp(`${prefix}Proxy\\s*:\\s*(\\S+)`))?.[1];
467
+ const port = Number(output.match(new RegExp(`${prefix}Port\\s*:\\s*(\\d+)`))?.[1]);
468
+ return enabled && host && Number.isInteger(port) && port > 0 ? { host, port } : null;
469
+ }
470
+ function execFileText(command, args) {
471
+ return new Promise((resolve) => {
472
+ execFile(command, args, { timeout: 1000 }, (error, stdout) => {
473
+ resolve(error ? "" : stdout);
474
+ });
475
+ });
476
+ }
477
+ function canConnectProxy(host, port) {
478
+ return new Promise((resolve) => {
479
+ const socket = createConnection({ host, port });
480
+ let settled = false;
481
+ const finish = (reachable) => {
482
+ if (settled) {
483
+ return;
484
+ }
485
+ settled = true;
486
+ socket.destroy();
487
+ resolve(reachable);
488
+ };
489
+ socket.setTimeout(750);
490
+ socket.once("connect", () => finish(true));
491
+ socket.once("error", () => finish(false));
492
+ socket.once("timeout", () => finish(false));
493
+ });
494
+ }