fix: harden cross-platform paths and child-process handling

Replace process.env.HOME fallbacks with os.homedir() in dashboard usage
probes and the hermes plugin profile resolver so unset HOME no longer
yields literal "~" paths. Skip POSIX process-group semantics on Windows
in engine/merger and dashboard-tui's pgrep-based vitest killer. Add
shell: true to npx spawns in CLI skills/extension so .cmd shims resolve
on Windows, and route test:build-exe through cross-env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-27 23:27:27 -07:00
parent f1bef9e422
commit 4e2131f444
8 changed files with 41 additions and 11 deletions

View File

@@ -45,7 +45,7 @@
"build:exe:all": "bun run build.ts --all",
"typecheck": "tsc --noEmit",
"test": "vitest run --silent=passed-only --reporter=dot",
"test:build-exe": "FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot"
"test:build-exe": "cross-env FUSION_TEST_BUILD_EXE=1 vitest run --config vitest.build-exe.config.ts --silent=passed-only --reporter=dot"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.70.0",
@@ -82,6 +82,7 @@
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@vitest/coverage-v8": "^3.1.0",
"cross-env": "^7.0.0",
"ink-testing-library": "^4.0.0",
"tsup": "^8.5.1",
"tsx": "^4.19.0",

View File

@@ -272,6 +272,10 @@ export class DashboardTUI {
* gone by the time we send the signal).
*/
killVitestProcesses(): { killed: number; pids: number[] } {
// pgrep is POSIX-only; Windows path is a no-op above.
if (process.platform === "win32") {
return { killed: 0, pids: [] };
}
const selfPid = process.pid;
let pids: number[] = [];
try {

View File

@@ -187,6 +187,7 @@ export async function runSkillsInstall(
const child = spawn("npx", npxArgs, {
cwd: process.cwd(),
stdio: "inherit",
shell: true,
});
const exitCode = await new Promise<number>((resolve, reject) => {

View File

@@ -1791,6 +1791,7 @@ export default function kbExtension(pi: ExtensionAPI) {
const child = spawn("npx", npxArgs, {
cwd: resolveProjectRoot(ctx.cwd),
stdio: "pipe",
shell: true,
});
let stderr = "";
@@ -1895,6 +1896,7 @@ export default function kbExtension(pi: ExtensionAPI) {
stdio: ["ignore", "pipe", "pipe"],
detached: false,
env: { ...process.env },
shell: true,
});
dashboardProcess = child;

View File

@@ -1,3 +1,4 @@
import * as os from "node:os";
import * as path from "node:path";
import { readFile } from "node:fs/promises";
import * as https from "node:https";
@@ -845,8 +846,8 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
// ── Credential reading for plan detection & auth check ──────────────
const credPaths = [
path.join(process.env.HOME || "~", ".claude", ".credentials.json"),
path.join(process.env.HOME || "~", ".config", "claude", ".credentials.json"),
path.join(os.homedir(), ".claude", ".credentials.json"),
path.join(os.homedir(), ".config", "claude", ".credentials.json"),
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped credentials JSON
@@ -1066,7 +1067,7 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
};
// Load Codex auth
const codexHome = process.env.CODEX_HOME || path.join(process.env.HOME || "~", ".codex");
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
const authPath = path.join(codexHome, "auth.json");
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped auth JSON
@@ -1181,7 +1182,7 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
};
// Load Gemini OAuth credentials
const oauthPath = path.join(process.env.HOME || "~", ".gemini", "oauth_creds.json");
const oauthPath = path.join(os.homedir(), ".gemini", "oauth_creds.json");
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped OAuth JSON
let oauthCreds: any = null;
try {
@@ -1203,7 +1204,7 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
}
// Check auth type from settings
const settingsPath = path.join(process.env.HOME || "~", ".gemini", "settings.json");
const settingsPath = path.join(os.homedir(), ".gemini", "settings.json");
try {
const settings = JSON.parse(await readFile(settingsPath, "utf-8"));
const authType = settings?.security?.auth?.selectedType;

View File

@@ -27,10 +27,12 @@ async function execWithProcessGroup(
return;
}
const useProcessGroup = process.platform !== "win32";
const child = spawn(command, {
cwd: options.cwd,
shell: true,
detached: true,
detached: useProcessGroup,
stdio: ["ignore", "pipe", "pipe"],
});
@@ -44,7 +46,13 @@ async function execWithProcessGroup(
const killTree = (sig: NodeJS.Signals) => {
if (child.pid === undefined) return;
try { process.kill(-child.pid, sig); } catch { /* group may already be gone */ }
try {
if (useProcessGroup) {
process.kill(-child.pid, sig);
} else {
child.kill(sig);
}
} catch { /* group may already be gone */ }
};
const timer = setTimeout(() => {

View File

@@ -12,7 +12,8 @@
*/
import { spawn, spawnSync } from "node:child_process";
import { sep as PATH_SEP } from "node:path";
import os from "node:os";
import path, { sep as PATH_SEP } from "node:path";
/**
* On Windows, `spawn("hermes", ...)` won't find `hermes.cmd`/`.bat` shims —
@@ -143,9 +144,9 @@ function parseProfileListOutput(raw: string): HermesProfileSummary[] {
* This is used to set HERMES_HOME when spawning hermes with a specific profile.
*/
function hermesProfileHome(profileName: string): string {
const base = process.env.HERMES_HOME ?? `${process.env.HOME ?? "~"}/.hermes`;
const base = process.env.HERMES_HOME ?? path.join(os.homedir(), ".hermes");
if (profileName === "default" || profileName === "") return base;
return `${base}/profiles/${profileName}`;
return path.join(base, "profiles", profileName);
}
/**

12
pnpm-lock.yaml generated
View File

@@ -81,6 +81,9 @@ importers:
'@vitest/coverage-v8':
specifier: ^3.1.0
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3))
cross-env:
specifier: ^7.0.0
version: 7.0.3
ink-testing-library:
specifier: ^4.0.0
version: 4.0.0(@types/react@19.2.14)
@@ -3392,6 +3395,11 @@ packages:
cross-dirname@0.1.0:
resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -10192,6 +10200,10 @@ snapshots:
cross-dirname@0.1.0:
optional: true
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1