diff --git a/.github/actions/setup-apple-replay/action.yml b/.github/actions/setup-apple-replay/action.yml index c82373032..22043d265 100644 --- a/.github/actions/setup-apple-replay/action.yml +++ b/.github/actions/setup-apple-replay/action.yml @@ -13,7 +13,7 @@ inputs: required: false default: "" build-command: - description: "Command used to build replay artifacts when the cache is cold" + description: "Command used to refresh replay artifacts before use" required: true xcuitest-platform: description: "Optional AGENT_DEVICE_XCUITEST_PLATFORM value" @@ -23,12 +23,8 @@ inputs: description: "Optional AGENT_DEVICE_XCUITEST_DESTINATION value" required: false default: "" - clean-derived: - description: "Optional AGENT_DEVICE_IOS_CLEAN_DERIVED value" - required: false - default: "" - build-on-miss: - description: "Whether this setup action should build replay artifacts on cache miss" + build-before-use: + description: "Whether this setup action should refresh replay artifacts after restoring the cache" required: false default: "true" @@ -37,7 +33,7 @@ outputs: description: "Resolved agent-device home directory" value: ${{ steps.agent-home.outputs.dir }} cache-hit: - description: "Whether the replay cache was restored" + description: "Whether a replay cache was restored before any build refresh" value: ${{ steps.restore-prebuilt.outputs.cache-hit }} runs: @@ -56,7 +52,7 @@ runs: id: source-hash run: | set -euo pipefail - echo "value=${{ hashFiles('ios-runner/**', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" + echo "value=${{ hashFiles('ios-runner/**', 'scripts/build-xcuitest-apple.sh', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'src/platforms/ios/apple-runner-platform.ts', 'src/platforms/ios/runner-icon.ts', 'src/platforms/ios/runner-xctestrun.ts', 'src/platforms/ios/runner-xctestrun-products.ts', '.github/actions/setup-apple-replay/action.yml', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT" shell: bash - name: Cache replay prebuilt @@ -68,15 +64,14 @@ runs: - name: Resolve agent-device home id: agent-home - run: echo "dir=$HOME/.agent-device" >> "$GITHUB_OUTPUT" + run: echo "dir=${AGENT_DEVICE_STATE_DIR:-$HOME/.agent-device}" >> "$GITHUB_OUTPUT" shell: bash - name: Build replay artifacts - if: inputs.build-on-miss == 'true' && steps.restore-prebuilt.outputs.cache-hit != 'true' + if: inputs.build-before-use == 'true' run: ${{ inputs.build-command }} shell: bash env: AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ inputs.derived-path }} AGENT_DEVICE_XCUITEST_PLATFORM: ${{ inputs.xcuitest-platform }} AGENT_DEVICE_XCUITEST_DESTINATION: ${{ inputs.xcuitest-destination }} - AGENT_DEVICE_IOS_CLEAN_DERIVED: ${{ inputs.clean-derived }} diff --git a/.github/actions/upload-agent-device-artifacts/action.yml b/.github/actions/upload-agent-device-artifacts/action.yml index a65b5e58a..b4764ad5b 100644 --- a/.github/actions/upload-agent-device-artifacts/action.yml +++ b/.github/actions/upload-agent-device-artifacts/action.yml @@ -8,6 +8,10 @@ inputs: agent-home-dir: description: "Resolved agent-device home directory" required: true + runner-derived-path: + description: "Optional Apple runner derived data path to upload cache metadata and Xcode logs" + required: false + default: ".agent-device-no-runner-derived" runs: using: "composite" @@ -21,5 +25,7 @@ runs: ${{ inputs.agent-home-dir }}/daemon.log ${{ inputs.agent-home-dir }}/logs/** ${{ inputs.agent-home-dir }}/sessions/** + ${{ inputs.runner-derived-path }}/.agent-device-runner-cache.json + ${{ inputs.runner-derived-path }}/Logs/** test/artifacts/** test/screenshots/** diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index fc282096f..50970600a 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -28,6 +28,7 @@ jobs: timeout-minutes: 80 env: IOS_RUNTIME_VERSION: '26.2' + AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: - name: Checkout @@ -76,4 +77,5 @@ jobs: uses: ./.github/actions/upload-agent-device-artifacts with: artifact-name: ios-artifacts - agent-home-dir: ${{ steps.apple-replay.outputs.agent-home-dir }} + agent-home-dir: ${{ env.AGENT_DEVICE_STATE_DIR }} + runner-derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e17cefffd..79a840c18 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -27,6 +27,7 @@ jobs: runs-on: macos-26 timeout-minutes: 80 env: + AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/macos-runner-derived steps: - name: Checkout @@ -42,6 +43,8 @@ jobs: derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} cache-key-prefix: macos-runner-prebuilt build-command: pnpm build:xcuitest:macos + xcuitest-platform: macos + xcuitest-destination: platform=macOS,arch=arm64 - name: Build macOS helper run: pnpm build:macos-helper @@ -54,4 +57,5 @@ jobs: uses: ./.github/actions/upload-agent-device-artifacts with: artifact-name: macos-artifacts - agent-home-dir: ${{ steps.apple-replay.outputs.agent-home-dir }} + agent-home-dir: ${{ env.AGENT_DEVICE_STATE_DIR }} + runner-derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} diff --git a/.github/workflows/perf-nightly.yml b/.github/workflows/perf-nightly.yml index 1d3d4586d..2140f62cf 100644 --- a/.github/workflows/perf-nightly.yml +++ b/.github/workflows/perf-nightly.yml @@ -34,6 +34,7 @@ jobs: timeout-minutes: 80 env: IOS_RUNTIME_VERSION: "26.2" + AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: - name: Checkout @@ -52,7 +53,6 @@ jobs: build-command: sh ./scripts/build-xcuitest-apple.sh xcuitest-platform: ios xcuitest-destination: generic/platform=iOS Simulator - clean-derived: "1" - name: Boot iOS test simulator uses: ./.github/actions/boot-ios-test-simulator diff --git a/.github/workflows/replays-nightly.yml b/.github/workflows/replays-nightly.yml index fe1636d2b..8dbeff270 100644 --- a/.github/workflows/replays-nightly.yml +++ b/.github/workflows/replays-nightly.yml @@ -51,6 +51,7 @@ jobs: timeout-minutes: 80 env: IOS_RUNTIME_VERSION: '26.2' + AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived steps: - name: Checkout @@ -69,7 +70,6 @@ jobs: build-command: sh ./scripts/build-xcuitest-apple.sh xcuitest-platform: ios xcuitest-destination: generic/platform=iOS Simulator - build-on-miss: 'false' - name: Boot iOS test simulator uses: ./.github/actions/boot-ios-test-simulator @@ -97,13 +97,15 @@ jobs: uses: ./.github/actions/upload-agent-device-artifacts with: artifact-name: replay-nightly-ios-artifacts - agent-home-dir: ${{ steps.apple-replay.outputs.agent-home-dir }} + agent-home-dir: ${{ env.AGENT_DEVICE_STATE_DIR }} + runner-derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} nightly-macos: name: macOS Replay Suite runs-on: macos-26 timeout-minutes: 80 env: + AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/macos-runner-derived steps: - name: Checkout @@ -119,6 +121,8 @@ jobs: derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} cache-key-prefix: macos-runner-prebuilt build-command: pnpm build:xcuitest:macos + xcuitest-platform: macos + xcuitest-destination: platform=macOS,arch=arm64 - name: Build macOS helper run: pnpm build:macos-helper @@ -131,4 +135,5 @@ jobs: uses: ./.github/actions/upload-agent-device-artifacts with: artifact-name: replay-nightly-macos-artifacts - agent-home-dir: ${{ steps.apple-replay.outputs.agent-home-dir }} + agent-home-dir: ${{ env.AGENT_DEVICE_STATE_DIR }} + runner-derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }} diff --git a/AGENTS.md b/AGENTS.md index e397539fd..b5c677784 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,7 @@ Single-context repo. Read `CONTEXT.md` for domain language and testing/architect ## Toolchain Snapshot - Package manager: `pnpm` only. Do not add or restore `package-lock.json`. +- Packaged installs use `~/.agent-device` as the implicit daemon state dir. Source checkouts default to a worktree-scoped daemon state dir under `~/.agent-device/dev/-` so local branches do not block each other. Use `pnpm daemon:state-dir` to print the effective path for the current worktree; `--state-dir` and `AGENT_DEVICE_STATE_DIR` remain authoritative overrides. Daemons are isolated by worktree, but devices are not; target different devices or simulators when running multiple worktrees concurrently. After pulling the worktree-scoped daemon change for the first time, stop any legacy source-checkout daemon with `AGENT_DEVICE_STATE_DIR=~/.agent-device pnpm clean:daemon`. - Runtime baseline is Node >= 22. Prefer built-in Node APIs such as global `fetch`, Web Streams, and `AbortSignal.timeout` over compatibility wrappers unless the surrounding code needs a lower-level transport. - Lint/format stack is OXC: - config: `.oxlintrc.json`, `.oxfmtrc.json` @@ -187,7 +188,7 @@ Command-only flags (like `find --first`) that do not flow to the platform layer ## Manual Device Session Hygiene - Treat every manually opened `agent-device` session as a resource that must be closed, including exploratory sessions and failed verification attempts. -- For experiments, use a purpose-specific session name and, when practical, an isolated `--state-dir` under `/private/tmp` so stale metadata does not poison the default daemon. +- For experiments, use a purpose-specific session name and, when practical, an isolated `--state-dir` under `/private/tmp` when you need cleanup isolation beyond the current worktree's default daemon. - Keep track of each opened session in the working notes. Before final response, close each one with the same flags used to open it. - If `close` or a later command is blocked by stale daemon metadata, inspect running processes first with `ps -ax | rg "agent-device|xcodebuild test-without-building"`. Stop only exact stale PIDs that belong to the verification run, then run `pnpm clean:daemon`. - If cleanup cannot be completed, report the remaining session name, state dir, process IDs, and metadata paths as a blocker. diff --git a/package.json b/package.json index 35e7e787f..ebf07b37f 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "scripts": { "build": "rslib build", "clean:daemon": "node --experimental-strip-types scripts/clean-daemon.ts", + "daemon:state-dir": "node --experimental-strip-types scripts/print-daemon-state-dir.ts", "clean:xcuitest": "node scripts/clean-xcuitest-derived.mjs", "clean:xcuitest:ios": "node scripts/clean-xcuitest-derived.mjs ios", "clean:xcuitest:macos": "node scripts/clean-xcuitest-derived.mjs macos", diff --git a/scripts/print-daemon-state-dir.ts b/scripts/print-daemon-state-dir.ts new file mode 100644 index 000000000..e574daa3a --- /dev/null +++ b/scripts/print-daemon-state-dir.ts @@ -0,0 +1,5 @@ +import { resolveDaemonPaths } from '../src/daemon/config.ts'; + +const paths = resolveDaemonPaths(process.env.AGENT_DEVICE_STATE_DIR); + +process.stdout.write(`${paths.baseDir}\n`); diff --git a/scripts/write-xcuitest-cache-metadata.mjs b/scripts/write-xcuitest-cache-metadata.mjs index c2f6da80c..13bf818e4 100644 --- a/scripts/write-xcuitest-cache-metadata.mjs +++ b/scripts/write-xcuitest-cache-metadata.mjs @@ -4,7 +4,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; -const [platform, derivedPath, destination] = process.argv.slice(2); +const args = process.argv.slice(2); +const [platform, derivedPath, destination] = args; if (!platform || !derivedPath || !destination) { console.error('Usage: write-xcuitest-cache-metadata.mjs '); diff --git a/src/__tests__/cli-session-state-dir.test.ts b/src/__tests__/cli-session-state-dir.test.ts new file mode 100644 index 000000000..ad848b533 --- /dev/null +++ b/src/__tests__/cli-session-state-dir.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 { test } from 'vitest'; +import { runCliCapture } from './cli-capture.ts'; + +test('session state-dir prints the resolved source-checkout daemon state dir without daemon startup', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-state-dir-')); + const home = path.join(root, 'home'); + fs.mkdirSync(home, { recursive: true }); + try { + const result = await runCliCapture(['session', 'state-dir', '--json'], { + env: { HOME: home }, + }); + + assert.equal(result.code, null); + assert.equal(result.calls.length, 0); + const payload = JSON.parse(result.stdout) as { success: boolean; data: { stateDir: string } }; + assert.equal(payload.success, true); + assert.match(payload.data.stateDir, /\/\.agent-device\/dev\//); + assert.equal(payload.data.stateDir.startsWith(home), true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('session state-dir respects explicit state dir overrides', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-state-dir-')); + try { + const result = await runCliCapture(['session', 'state-dir', '--state-dir', './custom-state'], { + cwd: root, + }); + + assert.equal(result.code, null); + assert.equal(result.calls.length, 0); + assert.equal(result.stdout.trim(), path.join(fs.realpathSync.native(root), 'custom-state')); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/src/cli.ts b/src/cli.ts index e2110d2fa..307939f81 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -241,6 +241,10 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS): parsedBatchSteps = readBatchSteps(flags); } + if (tryPrintSessionStateDir({ command, positionals, flags: effectiveFlags, daemonPaths })) { + return; + } + if (shouldResolveRemoteAuth(command)) { const authResolution = await resolveRemoteAuthForCli({ command, @@ -374,6 +378,24 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS): ); } +function tryPrintSessionStateDir(options: { + command: string; + positionals: string[]; + flags: CliFlags; + daemonPaths: ReturnType; +}): boolean { + if (options.command !== 'session' || options.positionals[0] !== 'state-dir') return false; + if (options.positionals.length > 1) { + throw new AppError('INVALID_ARGS', 'session state-dir does not accept additional arguments.'); + } + if (options.flags.json) { + printJson({ success: true, data: { stateDir: options.daemonPaths.baseDir } }); + } else { + process.stdout.write(`${options.daemonPaths.baseDir}\n`); + } + return true; +} + function isDebugRequested(argv: string[]): boolean { try { const parsed = parseRawArgs(argv); diff --git a/src/commands/cli-grammar/apps.ts b/src/commands/cli-grammar/apps.ts index 37a1f5ca3..39fb42c35 100644 --- a/src/commands/cli-grammar/apps.ts +++ b/src/commands/cli-grammar/apps.ts @@ -110,10 +110,11 @@ function installInputFromCli( }; } -function readSessionAction(value: string | undefined): 'list' { +function readSessionAction(value: string | undefined): 'list' | 'state-dir' { const action = value ?? 'list'; if (action === 'list') return action; - throw new AppError('INVALID_ARGS', 'session only supports list'); + if (action === 'state-dir') return action; + throw new AppError('INVALID_ARGS', 'session only supports list or state-dir'); } function openPositionals(input: CommandInput): string[] { diff --git a/src/commands/command-descriptions.ts b/src/commands/command-descriptions.ts index aec34b68d..99c74593e 100644 --- a/src/commands/command-descriptions.ts +++ b/src/commands/command-descriptions.ts @@ -2,7 +2,7 @@ const COMMAND_DESCRIPTIONS = { devices: 'List available devices.', boot: 'Boot or prepare a selected device without using CLI positional arguments.', apps: 'List installed apps.', - session: 'List active sessions.', + session: 'List active sessions or print daemon state directory.', open: 'Open an app, deep link, URL, or platform surface.', prepare: 'Prepare platform helper infrastructure.', close: 'Close an app or end the active session.', diff --git a/src/daemon/__tests__/config.test.ts b/src/daemon/__tests__/config.test.ts new file mode 100644 index 000000000..084a15882 --- /dev/null +++ b/src/daemon/__tests__/config.test.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { test } from 'vitest'; +import { resolveDaemonPaths } from '../config.ts'; + +test('resolveDaemonPaths keeps explicit state directories authoritative', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-config-home-')); + try { + const paths = resolveDaemonPaths('~/custom-daemon', { env: { HOME: home } }); + assert.equal(paths.baseDir, path.join(home, 'custom-daemon')); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +}); + +test('resolveDaemonPaths keeps packaged installs on the global daemon state directory', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-config-home-')); + const packageRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-package-root-')); + try { + fs.writeFileSync(path.join(packageRoot, 'package.json'), '{"name":"agent-device"}\n'); + + const paths = resolveDaemonPaths(undefined, { + env: { HOME: home }, + projectRoot: packageRoot, + }); + + assert.equal(paths.baseDir, path.join(home, '.agent-device')); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(packageRoot, { recursive: true, force: true }); + } +}); + +test('resolveDaemonPaths scopes source checkout defaults by project root', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-config-home-')); + const firstRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-source-a-')); + const secondRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-source-b-')); + try { + for (const root of [firstRoot, secondRoot]) { + fs.writeFileSync(path.join(root, 'package.json'), '{"name":"agent-device"}\n'); + fs.mkdirSync(path.join(root, 'src'), { recursive: true }); + fs.writeFileSync(path.join(root, 'src', 'daemon.ts'), 'export {};\n'); + } + + const firstPaths = resolveDaemonPaths(undefined, { + env: { HOME: home }, + projectRoot: firstRoot, + }); + const secondPaths = resolveDaemonPaths(undefined, { + env: { HOME: home }, + projectRoot: secondRoot, + }); + + assert.match(firstPaths.baseDir, /^.+\/\.agent-device\/dev\/agent-device-source-a-/); + assert.match(secondPaths.baseDir, /^.+\/\.agent-device\/dev\/agent-device-source-b-/); + assert.notEqual(firstPaths.baseDir, secondPaths.baseDir); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + fs.rmSync(firstRoot, { recursive: true, force: true }); + fs.rmSync(secondRoot, { recursive: true, force: true }); + } +}); diff --git a/src/daemon/config.ts b/src/daemon/config.ts index 51571f5f7..7e13e96f7 100644 --- a/src/daemon/config.ts +++ b/src/daemon/config.ts @@ -1,5 +1,8 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; import path from 'node:path'; import { expandUserHomePath, resolveUserPath } from '../utils/path-resolution.ts'; +import { findProjectRoot } from '../utils/version.ts'; export type DaemonServerMode = 'socket' | 'http' | 'dual'; export type DaemonTransportPreference = 'auto' | 'socket' | 'http'; @@ -13,8 +16,18 @@ export type DaemonPaths = { sessionsDir: string; }; -export function resolveDaemonPaths(stateDir: string | undefined): DaemonPaths { - const baseDir = resolveStateDir(stateDir); +type EnvMap = Record; + +type ResolveDaemonPathsOptions = { + env?: EnvMap; + projectRoot?: string; +}; + +export function resolveDaemonPaths( + stateDir: string | undefined, + options: ResolveDaemonPathsOptions = {}, +): DaemonPaths { + const baseDir = resolveStateDir(stateDir, options); return { baseDir, infoPath: path.join(baseDir, 'daemon.json'), @@ -24,12 +37,43 @@ export function resolveDaemonPaths(stateDir: string | undefined): DaemonPaths { }; } -function resolveStateDir(raw: string | undefined): string { +function resolveStateDir(raw: string | undefined, options: ResolveDaemonPathsOptions): string { const value = (raw ?? '').trim(); if (!value) { - return path.join(expandUserHomePath('~'), '.agent-device'); + return resolveDefaultDaemonStateDir(options); + } + return resolveUserPath(value, { env: options.env }); +} + +function resolveDefaultDaemonStateDir(options: ResolveDaemonPathsOptions = {}): string { + const globalStateDir = path.join(expandUserHomePath('~', { env: options.env }), '.agent-device'); + const projectRoot = options.projectRoot ?? findProjectRoot(); + if (!isSourceCheckoutProjectRoot(projectRoot)) { + return globalStateDir; + } + return path.join(globalStateDir, 'dev', buildSourceCheckoutStateDirName(projectRoot)); +} + +function isSourceCheckoutProjectRoot(projectRoot: string): boolean { + return ( + fs.existsSync(path.join(projectRoot, 'package.json')) && + fs.existsSync(path.join(projectRoot, 'src', 'daemon.ts')) + ); +} + +function buildSourceCheckoutStateDirName(projectRoot: string): string { + const resolvedRoot = resolveRealPath(projectRoot); + const slug = path.basename(resolvedRoot).replaceAll(/[^a-zA-Z0-9._-]+/g, '-'); + const hash = crypto.createHash('sha1').update(resolvedRoot).digest('hex').slice(0, 12); + return `${slug || 'agent-device'}-${hash}`; +} + +function resolveRealPath(filePath: string): string { + try { + return fs.realpathSync.native(filePath); + } catch { + return path.resolve(filePath); } - return resolveUserPath(value); } export function resolveDaemonServerMode(raw: string | undefined): DaemonServerMode { diff --git a/src/platforms/ios/__tests__/runner-xctestrun.test.ts b/src/platforms/ios/__tests__/runner-xctestrun.test.ts index bb89aec52..f0a903027 100644 --- a/src/platforms/ios/__tests__/runner-xctestrun.test.ts +++ b/src/platforms/ios/__tests__/runner-xctestrun.test.ts @@ -216,6 +216,28 @@ test('setup metadata script matches expected iOS simulator cache metadata', asyn 'esac', ].join('\n'), ); + writeExecutable( + path.join(binDir, 'plutil'), + [ + '#!/bin/sh', + "cat <<'JSON'", + '{"TestConfigurations":[{"TestTargets":[{"ProductPaths":["__TESTROOT__/Debug-iphonesimulator/AgentDeviceRunner.app","__TESTROOT__/Debug-iphonesimulator/AgentDeviceRunnerUITests-Runner.app"]}]}]}', + 'JSON', + ].join('\n'), + ); + fs.writeFileSync( + path.join( + root, + 'AgentDeviceRunner_AgentDeviceRunnerUITests_iphonesimulator26.2-arm64.xctestrun', + ), + '{}', + ); + fs.mkdirSync(path.join(root, 'Debug-iphonesimulator', 'AgentDeviceRunner.app'), { + recursive: true, + }); + fs.mkdirSync(path.join(root, 'Debug-iphonesimulator', 'AgentDeviceRunnerUITests-Runner.app'), { + recursive: true, + }); const previousPath = process.env.PATH; process.env.PATH = `${binDir}${path.delimiter}${previousPath ?? ''}`; __resetRunnerToolchainFingerprintCacheForTests(); diff --git a/src/utils/__tests__/args.test.ts b/src/utils/__tests__/args.test.ts index 5424b174c..f1632b3e2 100644 --- a/src/utils/__tests__/args.test.ts +++ b/src/utils/__tests__/args.test.ts @@ -1494,6 +1494,13 @@ test('usage renders concise commands inline with descriptions', () => { assert.doesNotMatch(help, /agent-device-proxy/); }); +test('session command help includes daemon state directory discovery', () => { + const help = usageForCommand('session'); + if (help === null) throw new Error('Expected command help text'); + assert.match(help, /Usage:\s+agent-device session list \| session state-dir/); + assert.match(help, /effective daemon state directory/); +}); + test('command usage describes test suite flags', () => { const help = usageForCommand('test'); if (help === null) throw new Error('Expected command help text'); diff --git a/src/utils/cli-command-overrides.ts b/src/utils/cli-command-overrides.ts index fad60b7d6..93a3f826b 100644 --- a/src/utils/cli-command-overrides.ts +++ b/src/utils/cli-command-overrides.ts @@ -345,8 +345,10 @@ const CLI_COMMAND_OVERRIDES = { positionalArgs: ['setting', 'state', 'target?', 'mode?'], }, session: { - usageOverride: 'session list', - positionalArgs: ['list?'], + usageOverride: 'session list | session state-dir', + listUsageOverride: 'session list', + helpDescription: 'List active sessions or print the effective daemon state directory', + positionalArgs: ['list|state-dir?'], }, } as const satisfies Partial>; diff --git a/src/utils/cli-flags.ts b/src/utils/cli-flags.ts index 803313934..936a1fe7a 100644 --- a/src/utils/cli-flags.ts +++ b/src/utils/cli-flags.ts @@ -188,7 +188,8 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--state-dir'], type: 'string', usageLabel: '--state-dir ', - usageDescription: 'Daemon state directory (defaults to ~/.agent-device)', + usageDescription: + 'Daemon state directory (defaults to ~/.agent-device for packages, or a worktree-scoped dev dir from source)', }, { key: 'daemonBaseUrl', diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index f4ffc3d2c..e0456e175 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -848,4 +848,4 @@ For CLI-discoverable setup guidance, run `agent-device help physical-device`. - First-run XCTest setup/build can take longer than normal commands; keep the device connected and use `--debug` to inspect signing/build diagnostics if setup times out. - If you override the iOS runner derived-data path and also force cleanup, keep `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` under the project `.tmp/` directory. Other cleanup override paths are rejected with a recovery hint. - For daemon startup troubleshooting: - - follow stale metadata hints for `/daemon.json` and `/daemon.lock` (`state-dir` defaults to `~/.agent-device`) + - follow stale metadata hints for `/daemon.json` and `/daemon.lock` (`state-dir` defaults to `~/.agent-device` for packaged installs, or a worktree-scoped dir under `~/.agent-device/dev/` from source) diff --git a/website/docs/docs/installation.md b/website/docs/docs/installation.md index 7aa516ee0..1d7c6d39e 100644 --- a/website/docs/docs/installation.md +++ b/website/docs/docs/installation.md @@ -84,4 +84,5 @@ One-off `npx` usage is fine for humans and scripts that intentionally fetch from - If daemon startup reports stale metadata, remove stale files and retry: - `/daemon.json` - `/daemon.lock` - - default state dir is `~/.agent-device` unless `AGENT_DEVICE_STATE_DIR` or `--state-dir` is set + - default state dir is `~/.agent-device` for packaged installs; source checkouts default to a worktree-scoped dir under `~/.agent-device/dev/` unless `AGENT_DEVICE_STATE_DIR` or `--state-dir` is set + - after pulling the worktree-scoped daemon change in a source checkout, stop any legacy default daemon once with `AGENT_DEVICE_STATE_DIR=~/.agent-device pnpm clean:daemon`