diff --git a/package.json b/package.json index b4cd3c0bf..12fdf9740 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist", + "package:apple-runner:npm": "node scripts/package-apple-runner-source.mjs", "build:android-multitouch-helper": "sh ./scripts/build-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper", "package:android-multitouch-helper": "sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper", "package:android-multitouch-helper:npm": "rm -rf android-multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android-multitouch-helper/dist", @@ -116,7 +117,7 @@ "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:mcp-metadata && pnpm build", "check:unit": "pnpm test:unit && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", - "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm", + "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:apple-runner:npm && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm", "typecheck": "tsgo -p tsconfig.json", "test-app:install": "pnpm install --dir examples/test-app --ignore-workspace", "test-app:start": "pnpm --dir examples/test-app start", @@ -149,13 +150,6 @@ "files": [ "bin", "dist", - "apple-runner", - "!apple-runner/**/.build", - "!apple-runner/**/.swiftpm", - "!apple-runner/**/xcuserdata", - "!apple-runner/**/*.xcuserstate", - "!apple-runner/README.md", - "!apple-runner/RUNNER_PROTOCOL.md", "macos-helper", "!macos-helper/**/.build", "android-snapshot-helper/dist", diff --git a/scripts/package-apple-runner-source.mjs b/scripts/package-apple-runner-source.mjs new file mode 100644 index 000000000..bfc4b25a7 --- /dev/null +++ b/scripts/package-apple-runner-source.mjs @@ -0,0 +1,195 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const UNIT_TEST_CONDITION = 'AGENT_DEVICE_RUNNER_UNIT_TESTS'; +const SOURCE_DIR = path.join('apple-runner'); +const OUTPUT_DIR = path.join('dist', 'apple-runner'); +const SKIPPED_DIR_NAMES = new Set(['.build', '.swiftpm', 'xcuserdata']); +const SKIPPED_ROOT_FILES = new Set(['README.md', 'RUNNER_PROTOCOL.md']); + +export function packageAppleRunnerSource(options = {}) { + const root = path.resolve(options.root ?? process.cwd()); + const sourceRoot = path.join(root, SOURCE_DIR); + const outputRoot = path.join(root, OUTPUT_DIR); + if (!fs.existsSync(sourceRoot)) { + throw new Error(`Apple runner source not found at ${sourceRoot}`); + } + + fs.rmSync(outputRoot, { recursive: true, force: true }); + const summary = { + outputRoot, + copiedFiles: 0, + strippedFiles: 0, + strippedBlocks: 0, + }; + + copyDirectory(sourceRoot, outputRoot, '', summary); + return summary; +} + +export function stripRunnerUnitTestBlocks(source, filePath = '') { + const lines = source.match(/[^\n]*\n|[^\n]+/g) ?? []; + const state = { + output: [], + strippedBlocks: 0, + skippedDepth: 0, + }; + + for (const line of lines) { + consumeSwiftLine(state, line); + } + + if (state.skippedDepth !== 0) { + throw new Error(`Unterminated ${UNIT_TEST_CONDITION} block in ${filePath}`); + } + + return { + contents: state.output.join(''), + strippedBlocks: state.strippedBlocks, + }; +} + +function consumeSwiftLine(state, line) { + if (state.skippedDepth > 0) { + consumeSkippedConditionalLine(state, line); + return; + } + if (isRunnerUnitTestBlockStart(line)) { + state.skippedDepth = 1; + state.strippedBlocks += 1; + return; + } + state.output.push(line); +} + +function consumeSkippedConditionalLine(state, line) { + if (isConditionalStart(line)) { + state.skippedDepth += 1; + } + if (isConditionalEnd(line)) { + state.skippedDepth -= 1; + } +} + +function copyDirectory(sourceDir, outputDir, relativeDir, summary) { + fs.mkdirSync(outputDir, { recursive: true }); + const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); + + for (const entry of entries) { + copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary); + } +} + +function copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary) { + const relativePath = path.join(relativeDir, entry.name); + if (shouldSkipEntry(entry, relativePath)) { + return; + } + + const sourcePath = path.join(sourceDir, entry.name); + const outputPath = path.join(outputDir, entry.name); + if (entry.isDirectory()) { + copyDirectory(sourcePath, outputPath, relativePath, summary); + return; + } + if (entry.isFile()) { + copyFile(sourcePath, outputPath, summary); + } +} + +function copyFile(sourcePath, outputPath, summary) { + if (path.extname(sourcePath) !== '.swift') { + fs.copyFileSync(sourcePath, outputPath); + summary.copiedFiles += 1; + return; + } + + const source = fs.readFileSync(sourcePath, 'utf8'); + const stripped = stripRunnerUnitTestBlocks(source, sourcePath); + fs.writeFileSync(outputPath, stripped.contents); + summary.copiedFiles += 1; + if (stripped.strippedBlocks > 0) { + summary.strippedFiles += 1; + summary.strippedBlocks += stripped.strippedBlocks; + } +} + +function shouldSkipEntry(entry, relativePath) { + return shouldSkipDirectory(entry) || shouldSkipFile(entry, relativePath); +} + +function shouldSkipDirectory(entry) { + return entry.isDirectory() && SKIPPED_DIR_NAMES.has(entry.name); +} + +function shouldSkipFile(entry, relativePath) { + return entry.isFile() && (isXcodeUserStateFile(entry) || isSkippedRootFile(entry, relativePath)); +} + +function isXcodeUserStateFile(entry) { + return entry.name.endsWith('.xcuserstate'); +} + +function isSkippedRootFile(entry, relativePath) { + return !relativePath.includes(path.sep) && SKIPPED_ROOT_FILES.has(entry.name); +} + +function isRunnerUnitTestBlockStart(line) { + return new RegExp(`^\\s*#if\\s+${UNIT_TEST_CONDITION}(?:\\b|$)`).test(line); +} + +function isConditionalStart(line) { + return /^\s*#if\b/.test(line); +} + +function isConditionalEnd(line) { + return /^\s*#endif\b/.test(line); +} + +function parseArgs(argv) { + const parsed = { root: process.cwd(), quiet: false }; + let index = 0; + while (index < argv.length) { + index = parseArg(argv, index, parsed); + } + return parsed; +} + +function parseArg(argv, index, parsed) { + const arg = argv[index]; + if (arg === '--quiet') { + parsed.quiet = true; + return index + 1; + } + if (arg === '--root') { + return parseRootArg(argv, index, parsed); + } + throw new Error(`Unknown argument: ${arg}`); +} + +function parseRootArg(argv, index, parsed) { + const value = argv[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error('--root requires a path'); + } + parsed.root = value; + return index + 2; +} + +function isMainModule() { + return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +} + +if (isMainModule()) { + const options = parseArgs(process.argv.slice(2)); + const summary = packageAppleRunnerSource(options); + if (!options.quiet) { + const relativeOutput = path.relative(path.resolve(options.root), summary.outputRoot); + console.log( + `Packaged Apple runner source at ${relativeOutput} ` + + `(${summary.copiedFiles} files, stripped ${summary.strippedBlocks} unit-test blocks).`, + ); + } +} diff --git a/scripts/size-report.mjs b/scripts/size-report.mjs index a027307ba..17a574d57 100644 --- a/scripts/size-report.mjs +++ b/scripts/size-report.mjs @@ -105,6 +105,7 @@ function collectReport(root, options) { if (jsFiles.length === 0) { throw new Error('No dist/src JavaScript files found. Run `pnpm build` before measuring size.'); } + prepareGeneratedPackageAssets(root); const chunks = jsFiles .map((file) => { @@ -132,11 +133,24 @@ function collectReport(root, options) { generatedAt: new Date().toISOString(), js, npmPack: collectNpmPack(root), - ...(options.startupRuns > 0 ? { startup: collectStartupBenchmarks(root, options.startupRuns) } : {}), + ...(options.startupRuns > 0 + ? { startup: collectStartupBenchmarks(root, options.startupRuns) } + : {}), chunks: chunks.slice(0, 20), }; } +function prepareGeneratedPackageAssets(root) { + const packageAppleRunnerScript = path.join(root, 'scripts', 'package-apple-runner-source.mjs'); + if (!fs.existsSync(packageAppleRunnerScript)) { + return; + } + execFileSync(process.execPath, [packageAppleRunnerScript, '--quiet'], { + cwd: root, + stdio: ['ignore', 'ignore', 'inherit'], + }); +} + function collectStartupBenchmarks(root, runs) { return { runs, @@ -297,7 +311,9 @@ function formatDiff(base, current) { function formatStartupBenchmarks(startup, baseStartup) { if (!startup) return ''; - const baseByName = new Map((baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark])); + const baseByName = new Map( + (baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark]), + ); const rows = startup.benchmarks.map((benchmark) => { const base = baseByName.get(benchmark.name); return `| ${benchmark.name} | ${formatMaybeMs(base?.medianMs)} | ${formatMs(benchmark.medianMs)} | ${formatMsDiff(base?.medianMs, benchmark.medianMs)} |`; @@ -369,7 +385,9 @@ function readGitHubCommentConfig(explicitPrNumber) { function assertGitHubCommentConfig(token, repository, prNumber) { for (const value of [token, repository, prNumber]) { if (!value) { - throw new Error('GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.'); + throw new Error( + 'GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.', + ); } } } diff --git a/src/__tests__/apple-runner-package-source.test.ts b/src/__tests__/apple-runner-package-source.test.ts new file mode 100644 index 000000000..a86c44237 --- /dev/null +++ b/src/__tests__/apple-runner-package-source.test.ts @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { onTestFinished, test } from 'vitest'; +import { runCmd } from '../utils/exec.ts'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const packageScript = path.join(repoRoot, 'scripts', 'package-apple-runner-source.mjs'); + +test('package apple runner source strips unit-test blocks without mutating checkout source', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-package-')); + onTestFinished(() => fs.rmSync(root, { recursive: true, force: true })); + + writeFixtureFile(root, 'apple-runner/README.md', 'developer docs\n'); + writeFixtureFile(root, 'apple-runner/.build/cache.txt', 'cache\n'); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj', + '', + ); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate', + 'state\n', + ); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + [ + 'extension RunnerTests {', + ' func runtimeHelper() {}', + '#if AGENT_DEVICE_RUNNER_UNIT_TESTS', + ' func unitOnlyHelper() {', + ' #if os(iOS)', + ' print("nested platform guard should disappear with the unit-test block")', + ' #endif', + ' }', + '#endif', + ' #if os(macOS)', + ' func macOnlyRuntimeHelper() {}', + ' #endif', + '}', + '', + ].join('\n'), + ); + + await runCmd(process.execPath, [packageScript, '--root', root, '--quiet']); + + const sourceSwiftPath = path.join( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + ); + const packagedSwiftPath = path.join( + root, + 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + ); + const sourceSwift = fs.readFileSync(sourceSwiftPath, 'utf8'); + const packagedSwift = fs.readFileSync(packagedSwiftPath, 'utf8'); + + assert.match(sourceSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/); + assert.doesNotMatch(packagedSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/); + assert.doesNotMatch(packagedSwift, /unitOnlyHelper/); + assert.match(packagedSwift, /runtimeHelper/); + assert.match(packagedSwift, /#if os\(macOS\)/); + assert.ok( + fs.existsSync( + path.join(root, 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj'), + ), + ); + assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/README.md')), false); + assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/.build/cache.txt')), false); + assert.equal( + fs.existsSync( + path.join( + root, + 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate', + ), + ), + false, + ); +}); + +function writeFixtureFile(root: string, relativePath: string, contents: string): void { + const filePath = path.join(root, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents); +} diff --git a/src/platforms/apple/core/__tests__/runner-source.test.ts b/src/platforms/apple/core/__tests__/runner-source.test.ts new file mode 100644 index 000000000..5aac0244e --- /dev/null +++ b/src/platforms/apple/core/__tests__/runner-source.test.ts @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { onTestFinished, test } from 'vitest'; +import { + resolveAppleRunnerProjectPath, + resolveAppleRunnerSourceRoot, +} from '../runner/runner-source.ts'; + +test('resolveAppleRunnerSourceRoot prefers checkout source over packaged source', () => { + const root = makeTempRoot(); + const checkoutSource = path.join(root, 'apple-runner', 'AgentDeviceRunner'); + const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner'); + fs.mkdirSync(path.join(checkoutSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + + assert.equal(resolveAppleRunnerSourceRoot(root), checkoutSource); + assert.equal( + resolveAppleRunnerProjectPath(root), + path.join(checkoutSource, 'AgentDeviceRunner.xcodeproj'), + ); +}); + +test('resolveAppleRunnerSourceRoot falls back to packaged source', () => { + const root = makeTempRoot(); + const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner'); + fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + + assert.equal(resolveAppleRunnerSourceRoot(root), packagedSource); + assert.equal( + resolveAppleRunnerProjectPath(root), + path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), + ); +}); + +function makeTempRoot(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-source-')); + onTestFinished(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} diff --git a/src/platforms/apple/core/runner/runner-artifact.ts b/src/platforms/apple/core/runner/runner-artifact.ts index 1a75c9290..316d2e50d 100644 --- a/src/platforms/apple/core/runner/runner-artifact.ts +++ b/src/platforms/apple/core/runner/runner-artifact.ts @@ -40,6 +40,7 @@ import { resolveRunnerBuildDestination, resolveRunnerXctestrunHints, } from '../apple-runner-platform.ts'; +import { resolveAppleRunnerProjectPath } from './runner-source.ts'; export { prepareXctestrunWithEnv } from './runner-artifact-env.ts'; const runnerXctestrunBuildLocks = new Map>(); @@ -234,12 +235,7 @@ async function buildXctestrunArtifact(params: { reason: ExistingXctestrunState['reason']; }): Promise { const { device, options, projectRoot, expectedCacheMetadata, derived, cache, reason } = params; - const projectPath = path.join( - projectRoot, - 'apple-runner', - 'AgentDeviceRunner', - 'AgentDeviceRunner.xcodeproj', - ); + const projectPath = resolveAppleRunnerProjectPath(projectRoot); if (!fs.existsSync(projectPath)) { throw new AppError('COMMAND_FAILED', 'iOS runner project not found', { projectPath }); diff --git a/src/platforms/apple/core/runner/runner-cache-metadata.ts b/src/platforms/apple/core/runner/runner-cache-metadata.ts index 4177c6289..6151beaa8 100644 --- a/src/platforms/apple/core/runner/runner-cache-metadata.ts +++ b/src/platforms/apple/core/runner/runner-cache-metadata.ts @@ -13,6 +13,7 @@ import { resolveRunnerPlatformName, resolveRunnerSdkName, } from '../apple-runner-platform.ts'; +import { resolveAppleRunnerSourceRoot } from './runner-source.ts'; const DEFAULT_IOS_RUNNER_APP_BUNDLE_ID = 'com.callstack.agentdevice.runner'; const RUNNER_DERIVED_ROOT = path.join(os.homedir(), '.agent-device', 'apple-runner'); @@ -232,7 +233,7 @@ type RunnerSourceFingerprintCacheEntry = { const runnerSourceFingerprintCache = new Map(); function computeRunnerSourceFingerprint(projectRoot: string): string { - const runnerRoot = path.join(projectRoot, 'apple-runner', 'AgentDeviceRunner'); + const runnerRoot = resolveAppleRunnerSourceRoot(projectRoot); const files = collectRunnerSourceFiles(runnerRoot); const fileStatsFingerprint = computeRunnerSourceFileStatsFingerprint(runnerRoot, files); const cached = runnerSourceFingerprintCache.get(runnerRoot); diff --git a/src/platforms/apple/core/runner/runner-source.ts b/src/platforms/apple/core/runner/runner-source.ts new file mode 100644 index 000000000..ff8db8e96 --- /dev/null +++ b/src/platforms/apple/core/runner/runner-source.ts @@ -0,0 +1,17 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const APPLE_RUNNER_SOURCE_ROOT = path.join('apple-runner', 'AgentDeviceRunner'); +const PACKAGED_APPLE_RUNNER_SOURCE_ROOT = path.join('dist', 'apple-runner', 'AgentDeviceRunner'); + +export function resolveAppleRunnerSourceRoot(projectRoot: string): string { + const checkoutSourceRoot = path.join(projectRoot, APPLE_RUNNER_SOURCE_ROOT); + if (fs.existsSync(checkoutSourceRoot)) { + return checkoutSourceRoot; + } + return path.join(projectRoot, PACKAGED_APPLE_RUNNER_SOURCE_ROOT); +} + +export function resolveAppleRunnerProjectPath(projectRoot: string): string { + return path.join(resolveAppleRunnerSourceRoot(projectRoot), 'AgentDeviceRunner.xcodeproj'); +} diff --git a/src/recording/__tests__/overlay.test.ts b/src/recording/__tests__/overlay.test.ts index 70b5d2945..e24ca6707 100644 --- a/src/recording/__tests__/overlay.test.ts +++ b/src/recording/__tests__/overlay.test.ts @@ -27,7 +27,11 @@ vi.mock('../../utils/video.ts', () => ({ waitForPlayableVideo: vi.fn(async () => {}), })); -import { overlayRecordingTouches, resizeRecording } from '../overlay.ts'; +import { + buildRecordingScriptPathCandidates, + overlayRecordingTouches, + resizeRecording, +} from '../overlay.ts'; import { AppError } from '../../kernel/errors.ts'; import { runCmd } from '../../utils/exec.ts'; @@ -151,3 +155,23 @@ test('resize forwards the requested high export preset', async () => { expect.arrayContaining(['--max-size', '720', '--quality', 'high']), ); }); + +test('recording script candidates include packaged dist apple-runner source', () => { + const packageRoot = path.join(tmpDir, 'package'); + const scriptPath = path.join( + packageRoot, + 'dist/apple-runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift', + ); + fs.mkdirSync(path.dirname(scriptPath), { recursive: true }); + fs.writeFileSync(scriptPath, 'print("overlay")\n'); + + const candidates = buildRecordingScriptPathCandidates( + 'recording-overlay.swift', + path.join(packageRoot, 'dist/src'), + packageRoot, + tmpDir, + ); + const firstExisting = candidates.find((candidate) => fs.existsSync(candidate)); + + expect(firstExisting).toBe(scriptPath); +}); diff --git a/src/recording/overlay.ts b/src/recording/overlay.ts index 8d9932051..581d1ece7 100644 --- a/src/recording/overlay.ts +++ b/src/recording/overlay.ts @@ -4,23 +4,40 @@ import { fileURLToPath } from 'node:url'; import { runCmd } from '../utils/exec.ts'; import { AppError } from '../kernel/errors.ts'; import { buildSwiftToolEnv, compileSwiftSourceFile } from '../utils/swift-cache.ts'; +import { findProjectRoot } from '../utils/version.ts'; import { waitForPlayableVideo, waitForStableFile } from '../utils/video.ts'; import { DEFAULT_RECORDING_EXPORT_QUALITY, type RecordingExportQuality, } from '../core/recording-export-quality.ts'; -function resolveScriptPath(scriptName: string): string { - const moduleDir = path.dirname(fileURLToPath(import.meta.url)); - const scriptCandidates = [ - fileURLToPath(new URL(`./${scriptName}`, import.meta.url)), - path.resolve(moduleDir, `../../apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`), - path.resolve( - moduleDir, - `../../../apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`, - ), - path.resolve(process.cwd(), `apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`), +export function buildRecordingScriptPathCandidates( + scriptName: string, + moduleDir: string, + projectRoot: string, + cwd: string, +): string[] { + const sourceScriptPath = `apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`; + const packagedScriptPath = `dist/${sourceScriptPath}`; + return [ + path.resolve(moduleDir, scriptName), + path.resolve(projectRoot, sourceScriptPath), + path.resolve(moduleDir, `../${sourceScriptPath}`), + path.resolve(moduleDir, `../../${sourceScriptPath}`), + path.resolve(moduleDir, `../../../${sourceScriptPath}`), + path.resolve(projectRoot, packagedScriptPath), + path.resolve(cwd, sourceScriptPath), ]; +} + +function resolveRecordingScriptPath(scriptName: string): string { + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const scriptCandidates = buildRecordingScriptPathCandidates( + scriptName, + moduleDir, + findProjectRoot(), + process.cwd(), + ); for (const candidate of scriptCandidates) { if (fs.existsSync(candidate)) { @@ -29,7 +46,7 @@ function resolveScriptPath(scriptName: string): string { } throw new AppError('COMMAND_FAILED', `Missing recording helper script: ${scriptName}`, { - hint: 'Ensure apple-runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled with the package.', + hint: 'Ensure apple-runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled under dist/apple-runner in the package.', scriptName, searchedPaths: scriptCandidates, }); @@ -49,17 +66,17 @@ export function getRecordingOverlaySupportWarning( } function getOverlayScriptPath(): string { - overlayScriptPath ??= resolveScriptPath('recording-overlay.swift'); + overlayScriptPath ??= resolveRecordingScriptPath('recording-overlay.swift'); return overlayScriptPath; } function getTrimScriptPath(): string { - trimScriptPath ??= resolveScriptPath('recording-trim.swift'); + trimScriptPath ??= resolveRecordingScriptPath('recording-trim.swift'); return trimScriptPath; } function getResizeScriptPath(): string { - resizeScriptPath ??= resolveScriptPath('recording-resize.swift'); + resizeScriptPath ??= resolveRecordingScriptPath('recording-resize.swift'); return resizeScriptPath; }