- macOS GUI μ±μ Shell Env λ―Έμμμ macOSκ° GUI μ±μ launchdλ‘ μ§μ μ€νν λ μ¬μ©μ μ μ€μ (
~/.zshrc)μ λ‘λνμ§ μμ μ¬μ©μ μ μ νκ²½λ³μκ° μ λ¬λμ§ μλ OS λ 벨 λμ - ν°λ―Έλμ ν΅ν μ€νκ³Ό λ¬λ¦¬ Dock/λλΈν΄λ¦ μ€νμμλ§ λ°μνλ μ€ν κ²½λ‘(λΆλͺ¨ νλ‘μΈμ€)μ μ°¨μ΄
- Electron, Swift, Objective-C λ± λͺ¨λ macOS GUI μ±μ΄ 곡ν΅μ μΌλ‘ κ²ͺλ νμμ΄λ©°, Electron κ³ μ λ²κ·Έκ° μλ
ν΄λΉ κ°λ μ΄ νμν μ΄μ
- κ°λ° νκ²½(ν°λ―Έλ μ€ν)μμλ μ μ λμνμ§λ§ prod λΉλ(GUI μ€ν)μμ API ν€, 컀μ€ν PATH λ±μ΄ μ¬λΌμ Έ μΈμ¦ μ€ν¨λ 컀맨λ not foundκ° λ°μ
~/.zshrcμ envλ₯Ό μ무리 μ μ€μ ν΄λ prod μ±μλ λλ¬νμ§ μλλ€λ μ¬μ€μ λͺ¨λ₯΄λ©΄ μμΈ νμ μ ν° μ΄λ €μ
AS-IS (μμ μ μ€ν κ²½λ‘)
sequenceDiagram autonumber participant T as ν°λ―Έλ(dev) participant L as launchd(prod) participant Z as ~/.zshrc participant E as Electron App Note over T,E: dev μ€ν κ²½λ‘ T->>Z: λ‘κ·ΈμΈ μ μμ± β ~/.zshrc λ‘λ Z-->>T: OPENAI_API_KEY, PATH λ± env μ€μ λ¨ T->>E: npm run dev (μμ νλ‘μΈμ€ β env μμ β ) Note over L,E: prod μ€ν κ²½λ‘ (μμ μ ) L->>E: Dock λλΈν΄λ¦ β launchdκ° μ§μ μ€ν Note over E: ~/.zshrc λ‘λ μμ<br/>env = μμ€ν κΈ°λ³Έκ°λ§<br/>OPENAI_API_KEY β<br/>PATH = /usr/bin:/binλ§ β
TO-BE (μμ ν)
sequenceDiagram autonumber participant L as launchd participant E as Electron (Main) participant Z as /bin/zsh participant B as Backend Process L->>E: μ± μ€ν (env = μμ€ν κΈ°λ³Έκ°) E->>Z: app.whenReady() β spawn("/bin/zsh", ["-ilc", "env"]) Z-->>E: stdout: OPENAI_API_KEY=...\nPATH=...50κ° env E->>E: Object.assign(process.env, parsedEnv) E->>B: startBackend({ ...process.env }) Note over B: μ env ν¬ν¨λ μνλ‘ μ€ν β
launchd vs ν°λ―Έλ μ€νμ μ°¨μ΄
macOS νλ‘μΈμ€ κ³μΈ΅μμ λκ° μ±μ μ€ν(spawn)νλλκ° ν΅μ¬μ΄λ€.
| νλͺ© | ν°λ―Έλ μ€ν (dev) | launchd μ€ν (prod) |
|---|---|---|
| λΆλͺ¨ νλ‘μΈμ€ | μ¬μ©μ μ (zsh/bash) | launchd (PID 1) |
| ~/.zshrc λ‘λ | β | β |
| μ¬μ©μ μ μ env | μ λΆ μμ | μμ€ν κΈ°λ³Έκ°λ§ |
| PATH | /opt/homebrew/bin λ± ν¬ν¨ | /usr/bin:/bin:/usr/sbin:/sbin |
| API ν€ | μμ | μμ |
μ λμ€ env μμ μμΉ: μμ νλ‘μΈμ€λ λΆλͺ¨μ envλ₯Ό κ·Έλλ‘ λ³΅μ¬ν΄ μμνλ€. launchdλ μ¬μ©μ μμ΄ μλλ―λ‘, μ¬μ©μκ° μμ μ μν envλ₯Ό μ μ μλ€.
μ€μ κ²½νν νμ (Electron + Codex CLI μΌμ΄μ€)
[prod λΉλ, μμ μ ]
checkCodexAuth() νΈμΆ
β codex exec probe μ€ν
β OPENAI_CODEX_API_KEY μμ β not-authenticated β
μλ¬ λ‘κ·Έ (μλ¬ νΈλ€λ§ μΆκ° ν νμΈ):
Command failed: codex exec --model gpt-5.2-codex say ok
Not inside a trusted directory and --skip-git-repo-check was not specified.
μ΄ μΌμ΄μ€μλ 2κ°μ λ 립λ λ¬Έμ κ° μ¨κ²¨μ Έ μμλ€:
| κ΄λ¬Έ | λ¬Έμ | μμΈ |
|---|---|---|
| 1μ°¨ | env λ―Έμμ β API ν€ μμ | macOS launchd μ€κ³ |
| 2μ°¨ | trusted directory κ±°λΆ | codex CLI 보μ μ μ± (μ± λ²λ€ λ΄λΆλ git repo μλ) |
1μ°¨μμ μ΄λ―Έ μ€ν¨νλ―λ‘ 2μ°¨ λ¬Έμ κ° prodμμ μ¨κ²¨μ Έ μμκ³ , catch(() => false)κ° μλ¬λ₯Ό μΌμΌμ μμΈ νμ
μ΄ λ λ¦μ΄μ‘λ€.
ν΄κ²° ν¨ν΄: Login Shell Spawn
VS Code(resolveShellEnv()), JetBrains IDE, shell-env npm ν¨ν€μ§ λ± λͺ¨λ μ£Όμ GUI μ±μ΄ λμΌνκ² μ¬μ©νλ μ
κ³ νμ€ ν¨ν΄μ΄λ€.
// electron/services/shell-env.service.ts
async function loadShellEnv(): Promise<void> {
const shell = os.userInfo().shell ?? '/bin/zsh'
// -i: interactive, -l: login (~/.zshrc λ‘λ), -c: command
const child = spawn(shell, ['-ilc', 'echo DELIM; env; echo DELIM; exit'])
// stdout νμ± β key=value μΆμΆ
const parsed = parseEnvOutput(stdout) // ~50κ° env λ³μ
Object.assign(process.env, parsed) // process.envμ merge
// μ΄ν startBackend({ ...process.env }) β μλ μ ν
}
// app.whenReady() μμ μ νΈμΆ (λ°±μλ μμ μ )
app.whenReady().then(async () => {
await loadShellEnv() // 300-400ms μμ
await startBackend()
})μ fallback 체μΈ: κ°μ§λ μ β /bin/zsh β /bin/bash β process.env κ·Έλλ‘ μ¬μ©
Edge case:
- nushell λ± λΉνμ€ μ β
/bin/zshλ‘ fallback ~/.zshrcλ¬Έλ² μ€λ₯ β/bin/bashλ‘ fallback- 5μ΄ λ΄ μλ΅ μμ β timeout ν μ± μ μ μμ (env μμ΄)