march-cli 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -32,6 +32,7 @@
32
32
  "node-notifier": "^10.0.1",
33
33
  "node-pty": "^1.1.0",
34
34
  "typebox": "^1.0.58",
35
+ "undici": "^7.25.0",
35
36
  "web-tree-sitter": "^0.26.8"
36
37
  },
37
38
  "optionalDependencies": {
@@ -5,7 +5,7 @@ import { Type } from "typebox";
5
5
  import { toolText } from "./tool-result.mjs";
6
6
  import { applyReplaceTextPatch, applyReplaceRangePatch } from "./editing/diff-apply.mjs";
7
7
  import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
8
- import { buildDiagnosticsForPath } from "../context/diagnostics.mjs";
8
+ import { formatLspDiagnosticsForPath } from "../lsp/diagnostics-format.mjs";
9
9
 
10
10
  export { formatDiff } from "./editing/diff-format.mjs";
11
11
 
@@ -117,7 +117,7 @@ async function waitForDiagnosticsForPath({ lspService, path, timeoutMs, interval
117
117
  if (!lspService?.snapshot || !path) return "";
118
118
  const deadline = Date.now() + timeoutMs;
119
119
  for (;;) {
120
- const diagnostics = buildDiagnosticsForPath({ snapshot: lspService.snapshot(), path });
120
+ const diagnostics = formatLspDiagnosticsForPath({ snapshot: lspService.snapshot(), path });
121
121
  if (diagnostics) return diagnostics;
122
122
  const remaining = deadline - Date.now();
123
123
  if (remaining <= 0) return "";
@@ -8,6 +8,7 @@ import { ContextEngine } from "../context/engine.mjs";
8
8
  import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs";
9
9
  import { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
10
10
  import { LspService } from "../lsp/service.mjs";
11
+ import { formatLspServiceEvent } from "../lsp/status-message.mjs";
11
12
  import { formatRecallHints } from "../memory/markdown-store.mjs";
12
13
  import { appendProviderUserMessage, estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
13
14
  import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
@@ -48,7 +49,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
48
49
  compaction: { enabled: false },
49
50
  retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
50
51
  });
51
- const lspService = new LspService({ cwd });
52
+ const lspService = new LspService({ cwd, onEvent: (event) => ui.status?.(formatLspServiceEvent(event)) });
52
53
  const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, centerMemoryPath, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
53
54
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
54
55
  const sessionBinding = createSessionBinding(null);
@@ -218,6 +219,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
218
219
  },
219
220
  getExtensionDiagnostics() { return runtimeHost?.getDiagnostics?.() ?? []; },
220
221
  getExtensionLifecycleState() { return lifecycleAdapter.getState(); },
222
+ getLspStatus() { return lspService.snapshot(); },
221
223
  async switchPiSession(sessionPath) {
222
224
  if (!runtimeHost) throw new Error("pi runtime host is not enabled");
223
225
  nextTurnContextMode = "rebuild";
@@ -31,6 +31,7 @@ export function statusBarLine({
31
31
  mode = MODES.DO,
32
32
  contextTokens = null,
33
33
  activity = null,
34
+ lspStatus = null,
34
35
  }) {
35
36
  return formatStatusBarLine({
36
37
  engine: runner.engine,
@@ -43,6 +44,7 @@ export function statusBarLine({
43
44
  mode,
44
45
  contextTokens,
45
46
  activity,
47
+ lspStatus,
46
48
  });
47
49
  }
48
50
 
@@ -80,6 +82,7 @@ export function formatStatusBarLine({
80
82
  mode = MODES.DO,
81
83
  contextTokens = null,
82
84
  activity = null,
85
+ lspStatus = null,
83
86
  }) {
84
87
  const model = engine.modelId || "model?";
85
88
  const thinking = engine.thinkingLevel || "thinking?";
@@ -91,6 +94,8 @@ export function formatStatusBarLine({
91
94
  const modeSegment = `${mode === MODES.DISCUSS ? WARN : OK}${formatModeLabel(mode)}`;
92
95
  const runtime = `${C.cyan}${model}${DIM}·${thinking}`;
93
96
  const segments = [modeSegment, runtime];
97
+ const lspText = formatLspSegment(lspStatus);
98
+ if (lspText) segments.push(`${C.fg250}${lspText}`);
94
99
  const activityText = formatActivitySegment(activity);
95
100
  if (activityText) segments.push(`${C.fg250}${activityText}`);
96
101
  const compactTokens = formatCompactTokenCount(contextTokens);
@@ -100,6 +105,42 @@ export function formatStatusBarLine({
100
105
  return `${inner}${R}`;
101
106
  }
102
107
 
108
+ export function formatLspSegment(lspStatus) {
109
+ if (!lspStatus) return "";
110
+ const servers = lspStatus.servers ?? [];
111
+ const visible = servers.filter((server) => server.id);
112
+ if (visible.length === 0) return "lsp:off";
113
+ const parts = buildLspStatusParts(visible);
114
+ if (parts.length === 0) return "lsp:off";
115
+ return `lsp:${parts.join(",")}`;
116
+ }
117
+
118
+ function buildLspStatusParts(servers) {
119
+ const byId = new Map();
120
+ for (const server of servers) {
121
+ const id = shortLspId(server.id);
122
+ byId.set(id, mergeLspStatus(byId.get(id), server.status));
123
+ }
124
+ return [...byId.entries()]
125
+ .filter(([, status]) => status !== "unavailable")
126
+ .map(([id, status]) => `${id}${formatLspStatusMark(status)}`);
127
+ }
128
+
129
+ function mergeLspStatus(current, next) {
130
+ const rank = { failed: 4, starting: 3, busy: 2, ready: 1, idle: 1, unavailable: 0 };
131
+ if (!current) return next ?? "unavailable";
132
+ const currentRank = rank[current] ?? 0;
133
+ const nextRank = rank[next] ?? 0;
134
+ return nextRank > currentRank ? next : current;
135
+ }
136
+
137
+ function formatLspStatusMark(status) {
138
+ if (status === "failed") return "!";
139
+ if (status === "starting") return "…";
140
+ if (status === "unavailable") return "";
141
+ return "✓";
142
+ }
143
+
103
144
  function formatActivitySegment(activity) {
104
145
  if (!activity) return "";
105
146
  const label = String(activity.label ?? "").trim();
@@ -107,6 +148,11 @@ function formatActivitySegment(activity) {
107
148
  return [frame, label].filter(Boolean).join(" ");
108
149
  }
109
150
 
151
+ function shortLspId(id) {
152
+ if (id === "typescript") return "ts";
153
+ return String(id ?? "?");
154
+ }
155
+
110
156
  export function formatCompactTokenCount(tokens) {
111
157
  const value = Number(tokens);
112
158
  if (!Number.isFinite(value) || value <= 0) return "";
@@ -45,7 +45,12 @@ export async function selectWithKeyboard({ input = process.stdin, output = proce
45
45
  input.off("data", onData);
46
46
  input.setRawMode(false);
47
47
  input.pause();
48
- if (renderedLines > 0) output.write(`\x1b[${renderedLines}F`);
48
+ if (renderedLines > 0) {
49
+ output.write(`\x1b[${renderedLines}F`);
50
+ // clear all previously rendered lines
51
+ for (let i = 0; i < renderedLines; i++) output.write("\x1b[2K\r\n");
52
+ output.write(`\x1b[${renderedLines}F`);
53
+ }
49
54
  // final render shows just the selected item
50
55
  const lines = formatSelectionList({ message, items, selected, viewportStart: 0, viewportEnd: items.length, done: value != null });
51
56
  output.write(`\x1b[2K\r${lines[0]}\n`);
@@ -27,6 +27,7 @@ export function createStatusLineUpdater({
27
27
  mode: getMode(),
28
28
  contextTokens,
29
29
  activity: formatActivity(activity, frameIndex),
30
+ lspStatus: runner.getLspStatus?.() ?? null,
30
31
  });
31
32
  ui.setStatusBar(line);
32
33
  return line;
@@ -46,6 +46,7 @@ function mergeLayers(layers) {
46
46
  serviceTier: null,
47
47
  providers: {},
48
48
  webSearch: { provider: null, providers: {} },
49
+ network: { proxy: "system", ca: "system" },
49
50
  maxTurns: null,
50
51
  trimBatch: null,
51
52
  memoryRoot: null,
@@ -69,6 +70,12 @@ function mergeLayers(layers) {
69
70
  ...layer.notifications,
70
71
  };
71
72
  }
73
+ if (layer.network && typeof layer.network === "object" && !Array.isArray(layer.network)) {
74
+ result.network = {
75
+ ...result.network,
76
+ ...layer.network,
77
+ };
78
+ }
72
79
  if (layer.memoryRoot) result.memoryRoot = layer.memoryRoot;
73
80
  }
74
81
 
@@ -1,6 +1,6 @@
1
1
  const MAX_DIAGNOSTICS = 20;
2
2
 
3
- export function buildDiagnosticsLayer({ snapshot } = {}) {
3
+ export function formatLspDiagnostics({ snapshot } = {}) {
4
4
  const diagnostics = snapshot?.diagnostics ?? [];
5
5
  if (diagnostics.length === 0) return "[diagnostics]";
6
6
 
@@ -17,12 +17,12 @@ export function buildDiagnosticsLayer({ snapshot } = {}) {
17
17
  return lines.join("\n");
18
18
  }
19
19
 
20
- export function buildDiagnosticsForPath({ snapshot, path } = {}) {
20
+ export function formatLspDiagnosticsForPath({ snapshot, path } = {}) {
21
21
  const targetPath = String(path ?? "");
22
22
  if (!targetPath) return "";
23
23
  const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => diagnostic.path === targetPath);
24
24
  if (diagnostics.length === 0) return "";
25
- return buildDiagnosticsLayer({
25
+ return formatLspDiagnostics({
26
26
  snapshot: {
27
27
  ...snapshot,
28
28
  diagnostics,
@@ -11,7 +11,11 @@ const LSP_SERVERS = [
11
11
  rootMarkers: NODE_ROOT_MARKERS,
12
12
  command: ["vue-language-server"],
13
13
  args: ["--stdio"],
14
- initialization: () => ({}),
14
+ initialization: ({ root, workspaceRoot }) => {
15
+ const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
16
+ return tsdk ? { typescript: { tsdk } } : null;
17
+ },
18
+ missingInitialization: "missing project typescript SDK",
15
19
  },
16
20
  {
17
21
  id: "typescript",
@@ -20,10 +24,10 @@ const LSP_SERVERS = [
20
24
  command: ["typescript-language-server"],
21
25
  args: ["--stdio"],
22
26
  initialization: ({ root, workspaceRoot }) => {
23
- const tsserver = resolveModule("typescript/lib/tsserver.js", workspaceRoot) ?? resolveModule("typescript/lib/tsserver.js", root);
24
- if (!tsserver) return null;
25
- return { tsserver: { path: tsserver } };
27
+ const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
28
+ return tsserver ? { tsserver: { path: tsserver } } : null;
26
29
  },
30
+ missingInitialization: "missing project typescript/tsserver.js",
27
31
  },
28
32
  {
29
33
  id: "python",
@@ -68,10 +72,10 @@ const LSP_SERVERS = [
68
72
  command: ["astro-ls", "@astrojs/language-server"],
69
73
  args: ["--stdio"],
70
74
  initialization: ({ root, workspaceRoot }) => {
71
- const tsserver = resolveModule("typescript/lib/tsserver.js", workspaceRoot) ?? resolveModule("typescript/lib/tsserver.js", root);
72
- if (!tsserver) return null;
73
- return { typescript: { tsdk: dirname(tsserver) } };
75
+ const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
76
+ return tsdk ? { typescript: { tsdk } } : null;
74
77
  },
78
+ missingInitialization: "missing project typescript SDK",
75
79
  },
76
80
  {
77
81
  id: "yaml",
@@ -126,23 +130,30 @@ const LSP_SERVERS = [
126
130
  ];
127
131
 
128
132
  export function resolveLspServer({ filePath, workspaceRoot }) {
133
+ const result = resolveLspServerStatus({ filePath, workspaceRoot });
134
+ return result.status === "available" ? result.server : null;
135
+ }
136
+
137
+ export function resolveLspServerStatus({ filePath, workspaceRoot }) {
129
138
  const ext = extensionOf(filePath);
130
139
  const def = LSP_SERVERS.find((server) => server.extensions.includes(ext));
131
- if (!def) return null;
140
+ if (!def) return { status: "unsupported", extension: ext };
132
141
 
133
142
  const root = def.rootMarkers.length > 0
134
143
  ? findNearestRoot(dirname(filePath), workspaceRoot, def.rootMarkers) ?? workspaceRoot
135
144
  : workspaceRoot;
136
145
  const command = findCommand(def.command, { root, workspaceRoot });
137
- if (!command) return null;
146
+ if (!command) {
147
+ return { status: "unavailable", id: def.id, root, reason: `missing ${def.command[0]}` };
148
+ }
149
+
138
150
  const initialization = def.initialization?.({ root, workspaceRoot }) ?? {};
139
- if (initialization === null) return null;
151
+ if (initialization === null) {
152
+ return { status: "unavailable", id: def.id, root, reason: def.missingInitialization ?? "missing SDK" };
153
+ }
140
154
  return {
141
- id: def.id,
142
- command,
143
- args: def.args,
144
- root,
145
- initialization,
155
+ status: "available",
156
+ server: { id: def.id, command, args: def.args, root, initialization },
146
157
  };
147
158
  }
148
159
 
@@ -160,7 +171,7 @@ function findCommand(names, { root, workspaceRoot }) {
160
171
 
161
172
  function findBin(name, { root, workspaceRoot }) {
162
173
  const names = platformCommandNames(name);
163
- for (const base of [root, workspaceRoot]) {
174
+ for (const base of uniquePaths([root, workspaceRoot])) {
164
175
  for (const bin of names) {
165
176
  const candidate = join(base, "node_modules", ".bin", bin);
166
177
  if (existsSync(candidate)) return candidate;
@@ -185,6 +196,24 @@ function findOnPath(names) {
185
196
  return null;
186
197
  }
187
198
 
199
+ function resolveTypeScriptServer({ root, workspaceRoot }) {
200
+ return resolveModuleFromRoots("typescript/lib/tsserver.js", { root, workspaceRoot });
201
+ }
202
+
203
+ function resolveTypeScriptSdk({ root, workspaceRoot }) {
204
+ const serverLibrary = resolveModuleFromRoots("typescript/lib/tsserverlibrary.js", { root, workspaceRoot });
205
+ const tsserver = serverLibrary ?? resolveTypeScriptServer({ root, workspaceRoot });
206
+ return tsserver ? dirname(tsserver) : null;
207
+ }
208
+
209
+ function resolveModuleFromRoots(id, { root, workspaceRoot }) {
210
+ for (const base of uniquePaths([root, workspaceRoot])) {
211
+ const hit = resolveModule(id, base);
212
+ if (hit) return hit;
213
+ }
214
+ return null;
215
+ }
216
+
188
217
  function resolveModule(id, base) {
189
218
  try {
190
219
  return createRequire(join(base, "package.json")).resolve(id);
@@ -193,6 +222,10 @@ function resolveModule(id, base) {
193
222
  }
194
223
  }
195
224
 
225
+ function uniquePaths(paths) {
226
+ return [...new Set(paths.map((path) => resolve(path)))];
227
+ }
228
+
196
229
  function findNearestRoot(start, stop, markers) {
197
230
  let dir = resolve(start);
198
231
  const boundary = resolve(stop);
@@ -1,28 +1,40 @@
1
1
  import { LspClient } from "./client.mjs";
2
2
  import { LspDiagnosticStore } from "./diagnostic-store.mjs";
3
- import { resolveLspServer } from "./servers.mjs";
3
+ import { resolveLspServerStatus } from "./servers.mjs";
4
4
 
5
5
  export class LspService {
6
- constructor({ cwd }) {
6
+ constructor({ cwd, onEvent = null }) {
7
7
  this.cwd = cwd;
8
+ this.onEvent = onEvent;
8
9
  this.store = new LspDiagnosticStore();
9
10
  this.clients = new Map();
10
11
  this.spawning = new Map();
12
+ this.unavailable = new Map();
13
+ this.announced = new Set();
11
14
  }
12
15
 
13
16
  touchFile(path) {
14
- const server = resolveLspServer({ filePath: path, workspaceRoot: this.cwd });
15
- if (!server) return;
17
+ const result = resolveLspServerStatus({ filePath: path, workspaceRoot: this.cwd });
18
+ if (result.status === "unsupported") return result;
19
+ if (result.status === "unavailable") {
20
+ this.unavailable.set(result.id, result);
21
+ this.#emitOnce(`unavailable:${result.id}:${result.reason}`, result);
22
+ return result;
23
+ }
24
+
25
+ const server = result.server;
16
26
  const key = `${server.id}:${server.root}`;
17
27
  const existing = this.clients.get(key);
18
28
  if (existing) {
19
29
  existing.touchFile(path);
20
- return;
30
+ return { status: "already_attached", id: server.id, root: server.root };
21
31
  }
22
32
  if (this.spawning.has(key)) {
23
33
  this.spawning.get(key).then((client) => client?.touchFile(path)).catch(() => {});
24
- return;
34
+ return { status: "starting", id: server.id, root: server.root };
25
35
  }
36
+
37
+ this.#emitOnce(`starting:${key}`, { status: "starting", id: server.id, root: server.root });
26
38
  const task = this.#startClient(server, key).then((client) => {
27
39
  client?.touchFile(path);
28
40
  return client;
@@ -31,13 +43,25 @@ export class LspService {
31
43
  task.finally(() => {
32
44
  if (this.spawning.get(key) === task) this.spawning.delete(key);
33
45
  }).catch(() => {});
46
+ return { status: "starting", id: server.id, root: server.root };
34
47
  }
35
48
 
36
49
  snapshot() {
37
50
  const diagnostics = this.store.snapshot();
38
- const statuses = [...this.clients.values()].map((client) => client.status);
39
- const status = statuses.includes("busy") ? "busy" : statuses.includes("starting") ? "starting" : statuses.includes("failed") ? "failed" : statuses.length > 0 ? "idle" : "";
40
- return { status, diagnostics };
51
+ const servers = [
52
+ ...[...this.clients.values()].map((client) => ({
53
+ id: client.serverId,
54
+ root: client.cwd,
55
+ status: client.status,
56
+ })),
57
+ ...[...this.spawning.keys()].map((key) => ({
58
+ id: key.slice(0, key.indexOf(":")),
59
+ root: key.slice(key.indexOf(":") + 1),
60
+ status: "starting",
61
+ })),
62
+ ...this.unavailable.values(),
63
+ ];
64
+ return { status: summarizeStatus(servers), diagnostics, servers };
41
65
  }
42
66
 
43
67
  async dispose() {
@@ -56,10 +80,31 @@ export class LspService {
56
80
  try {
57
81
  await client.start();
58
82
  this.clients.set(key, client);
83
+ this.unavailable.delete(server.id);
84
+ this.#emitOnce(`attached:${key}`, { status: "attached", id: server.id, root: server.root });
59
85
  return client;
60
- } catch {
86
+ } catch (err) {
61
87
  client.status = "failed";
88
+ const event = { status: "failed", id: server.id, root: server.root, reason: err.message };
89
+ this.unavailable.set(server.id, event);
90
+ this.#emitOnce(`failed:${key}:${err.message}`, event);
62
91
  return null;
63
92
  }
64
93
  }
94
+
95
+ #emitOnce(key, event) {
96
+ if (this.announced.has(key)) return;
97
+ this.announced.add(key);
98
+ this.onEvent?.(event);
99
+ }
100
+ }
101
+
102
+ function summarizeStatus(servers) {
103
+ const statuses = servers.map((server) => server.status);
104
+ if (statuses.includes("busy")) return "busy";
105
+ if (statuses.includes("starting")) return "starting";
106
+ if (statuses.includes("failed")) return "failed";
107
+ if (statuses.includes("ready") || statuses.includes("idle")) return "idle";
108
+ if (statuses.includes("unavailable")) return "unavailable";
109
+ return "";
65
110
  }
@@ -0,0 +1,8 @@
1
+ export function formatLspServiceEvent(event) {
2
+ const id = event?.id ? String(event.id) : "server";
3
+ if (event?.status === "attached") return `LSP attached: ${id}`;
4
+ if (event?.status === "starting") return `LSP starting: ${id}`;
5
+ if (event?.status === "failed") return `LSP failed: ${id} - ${event.reason}`;
6
+ if (event?.status === "unavailable") return `LSP unavailable: ${id} - ${event.reason}`;
7
+ return `LSP ${event?.status ?? "status"}: ${id}`;
8
+ }
package/src/main.mjs CHANGED
@@ -33,6 +33,7 @@ import { runProviderConfigCommand } from "./provider/config-command.mjs";
33
33
  import { runWebSearchConfigCommand } from "./web/config-command.mjs";
34
34
  import { createDesktopTurnNotifier } from "./notification/desktop-notifier.mjs";
35
35
  import { registerSuperGrokOAuthProvider } from "./supergrok/oauth-provider.mjs";
36
+ import { installNetworkEnvironment } from "./network/environment.mjs";
36
37
 
37
38
  export async function run(argv) {
38
39
  const cwd = process.cwd();
@@ -40,12 +41,13 @@ export async function run(argv) {
40
41
  registerSuperGrokOAuthProvider();
41
42
 
42
43
  const args = parseCliArgs(argv);
43
-
44
44
  if (args.help) {
45
45
  showHelp();
46
46
  return 0;
47
47
  }
48
48
 
49
+ const config = loadConfig(cwd);
50
+ installNetworkEnvironment(config.network);
49
51
  if (args.command?.name === "login") {
50
52
  try {
51
53
  return await runLoginCommand({
@@ -67,8 +69,6 @@ export async function run(argv) {
67
69
  const stateRoot = join(homedir(), ".march");
68
70
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
69
71
 
70
- // Load config (CLI args override config file values)
71
- const config = loadConfig(cwd);
72
72
  const provider = args.provider ?? config.provider ?? null;
73
73
  const serviceTier = config.serviceTier ?? null;
74
74
  const model = args.model ?? config.model ?? null;
@@ -0,0 +1,131 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import tls from "node:tls";
4
+ import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
5
+
6
+ export function installNetworkEnvironment(network = {}) {
7
+ const proxy = resolveProxySettings(network);
8
+ installProxyDispatcher(proxy);
9
+ const ca = installDefaultCertificates(network.ca ?? "system");
10
+ return { proxy, ca };
11
+ }
12
+
13
+ export function resolveProxySettings(network = {}, { env = process.env, platform = process.platform } = {}) {
14
+ const proxyMode = network.proxy ?? "system";
15
+ const explicitNoProxy = formatNoProxy(network.noProxy);
16
+
17
+ if (proxyMode === false || proxyMode === "none" || proxyMode === "direct") {
18
+ return { mode: "direct", httpProxy: null, httpsProxy: null, noProxy: explicitNoProxy ?? env.NO_PROXY ?? env.no_proxy ?? null };
19
+ }
20
+
21
+ if (typeof proxyMode === "string" && proxyMode.trim() && proxyMode !== "system") {
22
+ const proxy = normalizeProxyUrl(proxyMode.trim());
23
+ return { mode: "config", httpProxy: proxy, httpsProxy: proxy, noProxy: explicitNoProxy ?? env.NO_PROXY ?? env.no_proxy ?? null };
24
+ }
25
+
26
+ const envProxy = proxyFromEnv(env, explicitNoProxy);
27
+ if (envProxy.httpProxy || envProxy.httpsProxy) return { mode: "env", ...envProxy };
28
+
29
+ const systemProxy = platform === "win32" ? detectWindowsProxy() : null;
30
+ if (systemProxy?.httpProxy || systemProxy?.httpsProxy) {
31
+ return {
32
+ mode: "system",
33
+ httpProxy: systemProxy.httpProxy,
34
+ httpsProxy: systemProxy.httpsProxy,
35
+ noProxy: explicitNoProxy ?? systemProxy.noProxy ?? null,
36
+ };
37
+ }
38
+
39
+ return { mode: "direct", httpProxy: null, httpsProxy: null, noProxy: explicitNoProxy ?? null };
40
+ }
41
+
42
+ function installProxyDispatcher(proxy) {
43
+ setGlobalDispatcher(new EnvHttpProxyAgent({
44
+ httpProxy: proxy.httpProxy ?? "",
45
+ httpsProxy: proxy.httpsProxy ?? "",
46
+ noProxy: proxy.noProxy ?? "",
47
+ bodyTimeout: 0,
48
+ headersTimeout: 0,
49
+ }));
50
+ }
51
+
52
+ export function installDefaultCertificates(caConfig = "system") {
53
+ const entries = Array.isArray(caConfig) ? caConfig : [caConfig];
54
+ const wantsSystem = entries.includes("system");
55
+ const pemPaths = entries.filter((entry) => typeof entry === "string" && entry && entry !== "system");
56
+
57
+ if (!wantsSystem && pemPaths.length === 0) return { mode: "default", system: false, extraFiles: [] };
58
+ if (typeof tls.setDefaultCACertificates !== "function" || typeof tls.getCACertificates !== "function") {
59
+ return { mode: "unsupported", system: false, extraFiles: [] };
60
+ }
61
+
62
+ const certificates = [
63
+ ...tls.getCACertificates("default"),
64
+ ...(wantsSystem ? tls.getCACertificates("system") : []),
65
+ ...pemPaths.map((path) => readFileSync(path, "utf8")),
66
+ ];
67
+ tls.setDefaultCACertificates([...new Set(certificates)]);
68
+ return { mode: "installed", system: wantsSystem, extraFiles: pemPaths };
69
+ }
70
+
71
+ function proxyFromEnv(env, explicitNoProxy) {
72
+ const httpsProxy = env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? null;
73
+ const httpProxy = env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? null;
74
+ const normalizedHttp = httpProxy ? normalizeProxyUrl(httpProxy) : null;
75
+ return {
76
+ httpProxy: normalizedHttp,
77
+ httpsProxy: httpsProxy ? normalizeProxyUrl(httpsProxy) : normalizedHttp,
78
+ noProxy: explicitNoProxy ?? env.NO_PROXY ?? env.no_proxy ?? null,
79
+ };
80
+ }
81
+
82
+ function detectWindowsProxy() {
83
+ try {
84
+ const output = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], { encoding: "utf8", windowsHide: true });
85
+ const values = parseRegQuery(output);
86
+ if (values.ProxyEnable !== "0x1") return null;
87
+ return parseWindowsProxyServer(values.ProxyServer, values.ProxyOverride);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function parseRegQuery(output) {
94
+ const values = {};
95
+ for (const line of output.split(/\r?\n/)) {
96
+ const match = line.trim().match(/^(\S+)\s+REG_\S+\s+(.+)$/);
97
+ if (match) values[match[1]] = match[2].trim();
98
+ }
99
+ return values;
100
+ }
101
+
102
+ function parseWindowsProxyServer(proxyServer, proxyOverride) {
103
+ if (!proxyServer) return null;
104
+ const entries = Object.fromEntries(proxyServer.split(";").map((part) => part.split("=")).filter((part) => part.length === 2));
105
+ const fallback = proxyServer.includes("=") ? null : proxyServer;
106
+ return {
107
+ httpProxy: normalizeProxyUrl(entries.http ?? fallback),
108
+ httpsProxy: normalizeProxyUrl(entries.https ?? entries.http ?? fallback),
109
+ noProxy: formatWindowsProxyOverride(proxyOverride),
110
+ };
111
+ }
112
+
113
+ function normalizeProxyUrl(value) {
114
+ if (!value) return null;
115
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `http://${value}`;
116
+ }
117
+
118
+ function formatNoProxy(value) {
119
+ if (Array.isArray(value)) return value.join(",");
120
+ if (typeof value === "string") return value;
121
+ return null;
122
+ }
123
+
124
+ function formatWindowsProxyOverride(value) {
125
+ if (!value) return null;
126
+ return value
127
+ .split(";")
128
+ .map((entry) => entry.trim())
129
+ .filter((entry) => entry && entry !== "<local>")
130
+ .join(",") || null;
131
+ }