diff --git a/config/scripts/run-internal-dev-setup.mjs b/config/scripts/run-internal-dev-setup.mjs new file mode 100644 index 000000000..6bd3733dc --- /dev/null +++ b/config/scripts/run-internal-dev-setup.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import { accessSync, constants, existsSync } from 'node:fs' +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +function isExecutable(filePath, platform, access = accessSync) { + if (platform === 'win32') { + return true + } + + try { + access(filePath, constants.X_OK) + return true + } catch { + return false + } +} + +function quoteWindowsArg(value) { + return `"${value.replace(/"/g, '""')}"` +} + +function spawnOptionalSetup(spawn, setupPath, worktreePath, platform, env) { + if (platform === 'win32') { + spawn( + env.ComSpec || 'cmd.exe', + ['/d', '/s', '/c', `call ${quoteWindowsArg(setupPath)} ${quoteWindowsArg(worktreePath)}`], + { + stdio: 'inherit', + windowsVerbatimArguments: true + } + ) + return + } + + spawn(setupPath, [worktreePath], { + stdio: 'inherit' + }) +} + +export function runInternalDevSetup({ + env = process.env, + cwd = process.cwd(), + platform = process.platform, + exists = existsSync, + access = accessSync, + spawn = spawnSync +} = {}) { + const setupPath = env.ORCA_INTERNAL_DEV_SETUP?.trim() + if (!setupPath || !exists(setupPath) || !isExecutable(setupPath, platform, access)) { + return 0 + } + + // Why: this hook is an optional local accelerator; failures should not block + // creating a worktree or running the normal dependency install. + spawnOptionalSetup(spawn, setupPath, env.ORCA_WORKTREE_PATH || cwd, platform, env) + + return 0 +} + +if (process.argv[1] && resolve(fileURLToPath(import.meta.url)) === resolve(process.argv[1])) { + process.exit(runInternalDevSetup()) +} diff --git a/config/scripts/run-internal-dev-setup.test.mjs b/config/scripts/run-internal-dev-setup.test.mjs new file mode 100644 index 000000000..b00073156 --- /dev/null +++ b/config/scripts/run-internal-dev-setup.test.mjs @@ -0,0 +1,85 @@ +import { describe, expect, it, vi } from 'vitest' +import { runInternalDevSetup } from './run-internal-dev-setup.mjs' + +describe('runInternalDevSetup', () => { + it('does nothing when ORCA_INTERNAL_DEV_SETUP is unset', () => { + const spawn = vi.fn() + + expect(runInternalDevSetup({ env: {}, exists: vi.fn(), spawn })).toBe(0) + expect(spawn).not.toHaveBeenCalled() + }) + + it('does nothing when the configured setup path does not exist', () => { + const spawn = vi.fn() + + expect( + runInternalDevSetup({ + env: { ORCA_INTERNAL_DEV_SETUP: '/tmp/missing' }, + exists: () => false, + spawn + }) + ).toBe(0) + expect(spawn).not.toHaveBeenCalled() + }) + + it('skips non-executable setup paths on POSIX', () => { + const spawn = vi.fn() + + expect( + runInternalDevSetup({ + env: { ORCA_INTERNAL_DEV_SETUP: '/tmp/setup' }, + platform: 'linux', + exists: () => true, + access: () => { + throw new Error('EACCES') + }, + spawn + }) + ).toBe(0) + expect(spawn).not.toHaveBeenCalled() + }) + + it('runs the optional setup with the worktree path and ignores its exit status', () => { + const spawn = vi.fn(() => ({ status: 1 })) + + expect( + runInternalDevSetup({ + env: { + ORCA_INTERNAL_DEV_SETUP: '/tmp/setup', + ORCA_WORKTREE_PATH: '/tmp/worktree' + }, + platform: 'linux', + exists: () => true, + access: () => undefined, + spawn + }) + ).toBe(0) + expect(spawn).toHaveBeenCalledWith('/tmp/setup', ['/tmp/worktree'], { + stdio: 'inherit' + }) + }) + + it('uses cmd.exe with explicit quoting so .cmd setup shims receive paths with spaces', () => { + const spawn = vi.fn(() => ({ status: 0 })) + + expect( + runInternalDevSetup({ + env: { + ORCA_INTERNAL_DEV_SETUP: 'C:\\tools\\setup.cmd', + ORCA_WORKTREE_PATH: 'C:\\repo\\worktree' + }, + platform: 'win32', + exists: () => true, + spawn + }) + ).toBe(0) + expect(spawn).toHaveBeenCalledWith( + 'cmd.exe', + ['/d', '/s', '/c', 'call "C:\\tools\\setup.cmd" "C:\\repo\\worktree"'], + { + stdio: 'inherit', + windowsVerbatimArguments: true + } + ) + }) +}) diff --git a/orca.yaml b/orca.yaml index 5dad55e82..6b7ccd6f1 100644 --- a/orca.yaml +++ b/orca.yaml @@ -1,5 +1,4 @@ scripts: setup: | - [ -n "$ORCA_INTERNAL_DEV_SETUP" ] && [ -x "$ORCA_INTERNAL_DEV_SETUP" ] && \ - "$ORCA_INTERNAL_DEV_SETUP" "$ORCA_WORKTREE_PATH" || true + node config/scripts/run-internal-dev-setup.mjs pnpm install diff --git a/src/main/hooks-runner.test.ts b/src/main/hooks-runner.test.ts index cb8e1b8c9..da98c8ddb 100644 --- a/src/main/hooks-runner.test.ts +++ b/src/main/hooks-runner.test.ts @@ -185,33 +185,45 @@ describe('createIssueCommandRunnerScript', () => { it('writes a POSIX runner under the worktree git dir for long issue commands', async () => { const fs = await import('fs') + const originalPlatform = process.platform execFileSyncMock.mockReturnValue( '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh' ) - - const { createIssueCommandRunnerScript } = await import('./hooks') - const result = createIssueCommandRunnerScript( - makeRepo(), - '/test/repo-feature', - 'codex exec "long command"\nclaude -p "review it"' - ) - - expect(result).toEqual({ - runnerScriptPath: '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', - envVars: expect.objectContaining({ - ORCA_ROOT_PATH: '/test/repo', - ORCA_WORKTREE_PATH: '/test/repo-feature' - }) + Object.defineProperty(process, 'platform', { + configurable: true, + value: 'linux' }) - expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith( - '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', - '#!/usr/bin/env bash\nset -e\ncodex exec "long command"\nclaude -p "review it"\n', - 'utf-8' - ) - expect(vi.mocked(fs.chmodSync)).toHaveBeenCalledWith( - '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', - 0o755 - ) + + try { + const { createIssueCommandRunnerScript } = await import('./hooks') + const result = createIssueCommandRunnerScript( + makeRepo(), + '/test/repo-feature', + 'codex exec "long command"\nclaude -p "review it"' + ) + + expect(result).toEqual({ + runnerScriptPath: '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', + envVars: expect.objectContaining({ + ORCA_ROOT_PATH: '/test/repo', + ORCA_WORKTREE_PATH: '/test/repo-feature' + }) + }) + expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith( + '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', + '#!/usr/bin/env bash\nset -e\ncodex exec "long command"\nclaude -p "review it"\n', + 'utf-8' + ) + expect(vi.mocked(fs.chmodSync)).toHaveBeenCalledWith( + '/test/repo/.git/worktrees/feature/orca/issue-command-runner.sh', + 0o755 + ) + } finally { + Object.defineProperty(process, 'platform', { + configurable: true, + value: originalPlatform + }) + } }) })