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:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user