Files
AgentCompany/src/main/host-environment-service.ts
T
ZacharyZhang-NY 40040b3dd2 refactor(errors): surface 57 deferred catch-and-swallow sites via logger
The earlier sweep cleaned the hot path (worker/runtime/automation/api-
server/ipc core — 59 sites). This pass finishes the deferred long tail:
main/* services, database/* CRUD modules, api-server/* routes, ipc-*
handlers, thread-integration, automation helpers, browser, skills.

Default strategy: catch (error) { logger.warn("[module] context:", ...) }
— preserve current behavior, make the failure visible instead of
silently hidden. Widened 3 logger interface signatures to include warn
where callers only passed info/error previously (thread-helpers,
host-environment-service, ipc-domain-common).

Legitimate boundaries left intact: JSON.parse on untrusted input, DNS
resolver no-records paths, explicit safe* helpers (safeJson, parseJson,
parseTriggerDetail), URL-parse predicates, and `<unserialisable>`
fallbacks inside the prompt-builder summarisers. Each kept catch now
either has a reason in context or is a documented safe* helper.

Tests adjusted only where test-constructed no-op loggers needed a warn
stub (message-dispatch, company-mode-switch, ipc-agent-graph-handlers).

With the earlier pass, AgentCompany no longer has silently-swallowing
catch blocks in main-process or renderer business code.
2026-04-17 15:25:13 -04:00

110 lines
4.1 KiB
TypeScript

import { spawn } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { delimiter as pathDelimiter, join } from "node:path";
import { app } from "electron";
interface HostEnvironmentDependencies {
logger: {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
};
}
/**
* On Windows, Electron apps launched from installer shortcuts inherit a minimal PATH
* that often lacks user-installed CLI tool directories. This function enriches
* process.env.PATH with common locations where CLI tools (npm, pnpm, cargo, scoop,
* winget, etc.) are typically installed.
*/
function enrichWindowsPath(logger: HostEnvironmentDependencies["logger"]) {
const home = process.env.USERPROFILE ?? homedir();
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
const localAppData = process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
const pythonBase = join(localAppData, "Programs", "Python");
// Enumerate installed Python versions so new releases (Python313, ...)
// are picked up automatically without code changes.
const pythonScriptDirs: string[] = (() => {
try {
return readdirSync(pythonBase)
.filter((name) => /^Python\d+$/.test(name))
.map((name) => join(pythonBase, name, "Scripts"));
} catch (err) {
logger.warn(`[path] readdirSync failed for ${pythonBase}:`, err instanceof Error ? err.message : String(err));
return [];
}
})();
const candidateDirs = [
join(appData, "npm"),
join(localAppData, "pnpm"),
join(home, ".local", "bin"),
join(home, ".cargo", "bin"),
...pythonScriptDirs,
join(home, "scoop", "shims"),
join(home, ".bun", "bin"),
join(home, ".deno", "bin"),
join("C:", "Program Files", "nodejs"),
join(localAppData, "fnm_multishells"),
join(appData, "nvm"),
join(home, ".claude"),
join(localAppData, "Claude"),
];
const current = new Set(
(process.env.PATH ?? "").split(pathDelimiter).map((entry) => entry.toLowerCase()),
);
const toAdd = candidateDirs.filter(
(dir) => !current.has(dir.toLowerCase()) && existsSync(dir),
);
if (toAdd.length > 0) {
process.env.PATH = [process.env.PATH, ...toAdd].filter(Boolean).join(pathDelimiter);
logger.info(`[path] Windows: added ${toAdd.length} PATH entries: ${toAdd.join(", ")}`);
}
}
export async function prepareHostEnvironment({
logger,
}: HostEnvironmentDependencies) {
if (process.platform === "win32") {
enrichWindowsPath(logger);
app.setAppUserModelId("com.agentcompany.desktop");
} else if (process.env.AGENTCOMPANY_ENABLE_SHELL_PATH_PROBE === "true") {
try {
const userShell = process.env.SHELL || "/bin/zsh";
const shellPath = await new Promise<string>((resolve) => {
const child = spawn(userShell, ["-ilc", "echo -n $PATH"], {
env: { ...process.env },
stdio: ["ignore", "pipe", "ignore"],
timeout: 5000,
});
let output = "";
child.stdout?.on("data", (chunk: Buffer) => {
output += chunk.toString();
});
child.on("close", () => resolve(output.trim()));
child.on("error", () => resolve(""));
});
if (shellPath && shellPath.length > 0) {
const current = new Set((process.env.PATH ?? "").split(pathDelimiter).filter(Boolean));
const shellParts = shellPath.split(pathDelimiter).filter(Boolean);
const toAdd = shellParts.filter((entry) => !current.has(entry));
if (toAdd.length > 0) {
process.env.PATH = [process.env.PATH, ...toAdd].join(pathDelimiter);
logger.info(`[path] Added ${toAdd.length} shell PATH entries.`);
}
}
} catch (err) {
logger.warn(`[path] Shell PATH probe failed:`, err instanceof Error ? err.message : String(err));
}
} else {
logger.info("[path] Skipping interactive shell PATH probe; using deterministic PATH setup only.");
}
// Dock icon: macOS uses the .icns from the app bundle with the system squircle
// mask applied automatically. Do not call dock.setIcon() — it bypasses the mask
// and renders the raw PNG oversized and square.
}