Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
33 changes: 32 additions & 1 deletion src/main/workflow-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -22,6 +22,37 @@ function workflowDir(files: Record<string, string>): 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<void> => {}

Expand Down
24 changes: 24 additions & 0 deletions src/main/workflow-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading