diff --git a/src/main/index.ts b/src/main/index.ts index fc3b1dd..06e4a99 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -20,7 +20,7 @@ import { TaskBoard } from './task-board' import { buildTree } from './tree' import { UpdateService } from './update-service' import type { CtxDeps, GitFetchOptions, ShellResult } from './workflow-ctx' -import { discoverWorkflows, loadWorkflow } from './workflow-loader' +import { discoverWorkflows, esbuildBinaryPath, loadWorkflow } from './workflow-loader' import { WorkflowManager } from './workflow-manager' import { WorkflowRunStore } from './workflow-run-store' import { scaffoldWorkflow } from './workflow-scaffold' @@ -30,6 +30,18 @@ import { WorkspaceRegistry } from './workspace-registry' const execFileAsync = promisify(execFile) +// The workflow-loader bundles workflow.ts with esbuild, which spawns its native +// binary. In a packaged app esbuild resolves that binary to a path inside +// app.asar (not a real file → spawn ENOENT); point its env at the unpacked copy +// before the first build(). No-op in dev, where the binary sits in node_modules. +if (app.isPackaged && !process.env.ESBUILD_BINARY_PATH) { + process.env.ESBUILD_BINARY_PATH = esbuildBinaryPath( + process.resourcesPath, + process.platform, + process.arch + ) +} + /** * WF2 real `ctx.git.fetch` (WF2-07): a no-shell `git fetch`, mirroring the * worktree-manager `git` seam. `GIT_TERMINAL_PROMPT=0` fails fast instead of diff --git a/src/main/workflow-loader.test.ts b/src/main/workflow-loader.test.ts index 0768e84..5b039a9 100644 --- a/src/main/workflow-loader.test.ts +++ b/src/main/workflow-loader.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { discoverWorkflows, loadWorkflow, validateMeta } from './workflow-loader' +import { discoverWorkflows, esbuildBinaryPath, loadWorkflow, validateMeta } from './workflow-loader' const dirs: string[] = [] afterEach(() => { @@ -22,6 +22,37 @@ function workflowDir(files: Record): string { return folder } +describe('esbuildBinaryPath (pure)', () => { + it('points at the unpacked win32 .exe under app.asar.unpacked', () => { + const p = esbuildBinaryPath('C:\\app\\resources', 'win32', 'x64') + expect(p).toBe( + join( + 'C:\\app\\resources', + 'app.asar.unpacked', + 'node_modules', + '@esbuild', + 'win32-x64', + 'esbuild.exe' + ) + ) + }) + + it('uses the bin/esbuild layout on non-Windows platforms', () => { + const p = esbuildBinaryPath('/app/resources', 'darwin', 'arm64') + expect(p).toBe( + join( + '/app/resources', + 'app.asar.unpacked', + 'node_modules', + '@esbuild', + 'darwin-arm64', + 'bin', + 'esbuild' + ) + ) + }) +}) + describe('validateMeta (pure)', () => { const run = async (): Promise => {} diff --git a/src/main/workflow-loader.ts b/src/main/workflow-loader.ts index 7005f85..0961b9b 100644 --- a/src/main/workflow-loader.ts +++ b/src/main/workflow-loader.ts @@ -17,6 +17,30 @@ export type LoadedWorkflow = { meta: WorkflowMeta; run: RunFn } | { error: strin /** Node builtins + electron are left external — a bundled workflow never inlines them. */ const EXTERNAL = [...builtinModules, ...builtinModules.map((m) => `node:${m}`), 'electron'] +/** + * The on-disk path of esbuild's native binary inside a packaged app (WF fix). + * esbuild resolves its binary relative to its own package, which electron-builder + * places inside `app.asar`; a `.exe` there is not a real file, so `spawn` gets + * ENOENT. electron-builder DOES smart-unpack `@esbuild/*` to `app.asar.unpacked`, + * so we point esbuild's `ESBUILD_BINARY_PATH` env at that real copy. Pure so it + * can be unit-tested without Electron; the caller supplies `process.resourcesPath`. + */ +export function esbuildBinaryPath( + resourcesPath: string, + platform: NodeJS.Platform, + arch: string +): string { + const bin = platform === 'win32' ? 'esbuild.exe' : join('bin', 'esbuild') + return join( + resourcesPath, + 'app.asar.unpacked', + 'node_modules', + '@esbuild', + `${platform}-${arch}`, + bin + ) +} + /** * List the workflow folder names directly under `root` (workflow id = folder * name, WF2-01). Returns `[]` — never throws — when `root` is missing or empty.