mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-30 03:15:30 +00:00
fix(app): packaged app finds node when spawning the CLI
GUI-launched apps inherit a minimal PATH without the user's node install, so spawning the codeburn npm shim (#!/usr/bin/env node) failed with 'env: node: No such file or directory' in packaged builds. Spawn now augments PATH with the resolved binary's own directory (node sits beside the shim in nvm, Homebrew, and npm-prefix layouts) plus the resolver's search dirs. Dev never hit this because the terminal PATH was inherited.
This commit is contained in:
parent
4abf5050b4
commit
700a52cd95
2 changed files with 38 additions and 5 deletions
|
|
@ -2,9 +2,9 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
import { spawnCli, spawnCliAction, killAll, CliError, nodeManagerDirs, resolveCodeburnPath } from './cli'
|
||||
import { spawnCli, spawnCliAction, spawnEnvFor, killAll, CliError, nodeManagerDirs, resolveCodeburnPath } from './cli'
|
||||
|
||||
let dir: string
|
||||
const originalBin = process.env.CODEBURN_BIN
|
||||
|
|
@ -115,6 +115,24 @@ describe('spawnCli', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('spawn PATH augmentation (GUI-launched apps have a minimal PATH)', () => {
|
||||
it("prepends the resolved binary's own directory so its env-shebang finds node", async () => {
|
||||
const bin = fakeBin('path-echo.js', 'process.stdout.write(JSON.stringify({ path: process.env.PATH }))')
|
||||
const result = await spawnCli(['status']) as { path: string }
|
||||
expect(result.path.split(':')[0]).toBe(dirname(bin))
|
||||
})
|
||||
|
||||
it('spawnEnvFor dedupes and keeps the original PATH entries', () => {
|
||||
const env = spawnEnvFor('/some/tool/bin/codeburn')
|
||||
const parts = (env.PATH ?? '').split(':')
|
||||
expect(parts[0]).toBe('/some/tool/bin')
|
||||
expect(new Set(parts).size).toBe(parts.length)
|
||||
for (const original of (process.env.PATH ?? '').split(':').filter(Boolean)) {
|
||||
expect(parts).toContain(original)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('spawnCli coalescing (read-only)', () => {
|
||||
it('shares one child between two concurrent identical calls', async () => {
|
||||
const countFile = join(dir, 'spawns')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { spawn, type ChildProcess } from 'node:child_process'
|
||||
import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { homedir, platform } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
|
||||
// Runs entirely in the Electron main process. This module must NOT import
|
||||
// `electron` so it stays unit-testable in a plain node environment.
|
||||
|
|
@ -77,6 +77,21 @@ function searchDirs(): string[] {
|
|||
return [...pathDirs, ...nodeManagerDirs()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn env for the resolved CLI. A GUI-launched app inherits a minimal PATH
|
||||
* (/usr/bin:/bin:...) that lacks the user's node install, and the `codeburn`
|
||||
* npm shim starts with `#!/usr/bin/env node` — so spawning it fails with
|
||||
* "env: node: No such file or directory" even though the shim itself was
|
||||
* found. Prepend the shim's own directory (node sits beside it in nvm,
|
||||
* Homebrew, and npm-prefix layouts) plus the same dirs the resolver searches.
|
||||
*/
|
||||
export function spawnEnvFor(bin: string): NodeJS.ProcessEnv {
|
||||
const parts = [dirname(bin), ...searchDirs(), ...(process.env.PATH || '').split(delimiter)]
|
||||
const seen = new Set<string>()
|
||||
const path = parts.filter(p => p && !seen.has(p) && (seen.add(p), true)).join(delimiter)
|
||||
return { ...process.env, PATH: path }
|
||||
}
|
||||
|
||||
function isExecutableFile(p: string): boolean {
|
||||
try {
|
||||
if (!statSync(p).isFile()) return false
|
||||
|
|
@ -151,7 +166,7 @@ export function resolveCodeburnPath(): string | null {
|
|||
|
||||
function runCli(bin: string, args: string[], timeoutMs: number): Promise<unknown> {
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const child = spawn(bin, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const child = spawn(bin, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'], env: spawnEnvFor(bin) })
|
||||
activeChildren.add(child)
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
|
@ -245,7 +260,7 @@ export function spawnCliAction(args: string[], opts: { timeoutMs?: number } = {}
|
|||
return
|
||||
}
|
||||
|
||||
const child = spawn(bin, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const child = spawn(bin, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'], env: spawnEnvFor(bin) })
|
||||
activeChildren.add(child)
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue