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
16 changes: 10 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,27 +111,31 @@ hydrates root and `packages/client` `node_modules` plus
falling back to `npm ci` for missing package installs and ensuring Homebrew
`pkgconf`/`x264` are available for native builds. Its Run action executes
`npm run codex:run`, which builds the CLI and client, saves fresh caches, and
restarts the workspace-local daemon.
restarts the local service.

Run the local daemon:
Run the local service:

```sh
./build/simdeck
./build/simdeck daemon start --port 4311
./build/simdeck -p 4311
```

Running without a subcommand starts a foreground workspace daemon, prints local and LAN HTTP URLs, prints a six-digit pairing code for LAN browsers, and stops when the command exits, when you press `q`, or when you press Ctrl-C. If the always-on service is active on 4310, running without a subcommand or running `simdeck ui` prints the existing service endpoints instead of starting a project daemon. Pass a simulator name or UDID as the only argument to select it by default in the UI. Use `./build/simdeck -d`, `./build/simdeck -k`, and `./build/simdeck -r` as detached start, kill, and restart shortcuts.
Running without a subcommand starts or reuses the background service, prints
local and LAN HTTP URLs, and prints a six-digit pairing code for LAN browsers.
Pass a simulator name or UDID as the only argument to select it by default in
the UI. Use `./build/simdeck -a` or `./build/simdeck pair` when the service
should be registered as a LaunchAgent.

Use software H.264 when macOS screen recording starves the hardware encoder:

```sh
./build/simdeck daemon start --port 4311 --video-codec h264-software
./build/simdeck daemon restart --video-codec h264-software
```

For LAN access:

```sh
./build/simdeck daemon start --port 4311 --bind 0.0.0.0 --advertise-host 192.168.1.50
./build/simdeck -p 4311 --bind 0.0.0.0 --advertise-host 192.168.1.50
```

Useful direct commands:
Expand Down
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ To focus a specific simulator by name or UDID, pass it as the only argument:
simdeck "iPhone 17 Pro Max"
```

`simdeck -d` for detached start, `simdeck -k` to kill the background daemon, and `simdeck -r` to restart it.
Use `simdeck --open` to open the browser automatically, `simdeck -p 4311` to
use a non-default port, and `simdeck -a` to register the service for login
autostart.

The served loopback browser UI receives the generated API access token automatically.
LAN clients should pair with the printed code before receiving the API cookie.
Expand All @@ -77,20 +79,17 @@ For pairing with SimDeck iOS app:
simdeck pair
```

This starts or refreshes the global LaunchAgent-backed SimDeck service, prints
This starts or refreshes the LaunchAgent-backed SimDeck service, prints
local, LAN, and Tailscale URLs when available, and shows a QR code with a
`simdeck://pair` link. The QR contains the pairing code plus all detected
non-loopback addresses, so pairing once can save both the LAN and Tailscale
routes with the same service token.
Normal service restarts preserve that token so paired clients stay connected.
Use `simdeck service reset` only when you want to rotate the service token and
restart the LaunchAgent.
The LaunchAgent service uses port 4310. Project daemons start at port 4311 and
probe upward when that port is busy. When the service is active, `simdeck` and
`simdeck ui` print the existing service endpoints instead of starting a project
daemon; use the `daemon` subcommand when you explicitly want a workspace daemon.
The service uses port 4310 unless you pass `-p` or `--port`.

CLI commands automatically use the same warm daemon:
CLI commands automatically use the same warm service:

```sh
simdeck list
Expand Down Expand Up @@ -195,8 +194,8 @@ try {
}
```

`connect()` starts the project daemon when needed, reuses it when it is already
healthy, and only stops daemons it started itself. Pass `udid` to `connect()`
`connect()` starts the SimDeck service when needed and reuses it when it is
already healthy. Pass `udid` to `connect()`
to make it the default for session methods; each method still accepts an
explicit UDID as the first argument when needed. Query helpers such as
`tree()`, `query()`, `waitFor()`, `assert()`, and selector `tapElement()`
Expand Down Expand Up @@ -236,7 +235,7 @@ import "expo-router/entry";
Import it before `expo-router/entry` or `AppRegistry.registerComponent(...)`
so the package can capture React Fiber commits. The auto entrypoint no-ops
outside development, reads `EXPO_PUBLIC_SIMDECK_PORT` when present, and
otherwise scans common SimDeck daemon ports.
otherwise scans common SimDeck service ports.

## Flutter Inspector

Expand Down
100 changes: 96 additions & 4 deletions actions/run-android-comment-session/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ inputs:
description: Verify the public Cloudflare Tunnel health endpoint before continuing.
required: false
default: "false"
ci_proxy_url:
description: Optional SimDeck CI proxy Worker URL, such as https://ci.simdeck.sh.
required: false
default: https://simdeck-ci-proxy.djdeveloperr.workers.dev
proxy_links:
description: Post SimDeck CI proxy links instead of raw Cloudflare Tunnel links.
required: false
default: "true"
session_password:
description: Optional password required by the SimDeck CI proxy before it opens the tunnel.
required: false
default: ""

runs:
using: composite
Expand All @@ -112,6 +124,9 @@ runs:
INPUT_ANDROID_ARCH_VALUE: ${{ inputs.android_arch }}
INPUT_ANDROID_BUILD_TOOLS_VALUE: ${{ inputs.android_build_tools }}
INPUT_PUBLIC_HEALTH_CHECK_VALUE: ${{ inputs.public_health_check }}
INPUT_CI_PROXY_URL_VALUE: ${{ inputs.ci_proxy_url }}
INPUT_PROXY_LINKS_VALUE: ${{ inputs.proxy_links }}
INPUT_SESSION_PASSWORD_VALUE: ${{ inputs.session_password }}
KEEPALIVE_SECONDS_VALUE: ${{ inputs.keepalive_seconds }}
BUILD_WORKFLOW_VALUE: ${{ inputs.build_workflow }}
ARTIFACT_PREFIX_VALUE: ${{ inputs.artifact_prefix }}
Expand Down Expand Up @@ -148,6 +163,12 @@ runs:
write_env "SIMDECK_ANDROID_TARGET" "${INPUT_ANDROID_TARGET_VALUE}"
write_env "SIMDECK_ANDROID_ARCH" "${INPUT_ANDROID_ARCH_VALUE}"
write_env "SIMDECK_ANDROID_BUILD_TOOLS" "${INPUT_ANDROID_BUILD_TOOLS_VALUE}"
write_env "SIMDECK_CI_PROXY_URL" "${INPUT_CI_PROXY_URL_VALUE}"
write_env "SIMDECK_PROXY_LINKS" "${INPUT_PROXY_LINKS_VALUE}"
write_env "SIMDECK_SESSION_PASSWORD" "${INPUT_SESSION_PASSWORD_VALUE}"
if [[ -n "${INPUT_SESSION_PASSWORD_VALUE}" ]]; then
echo "::add-mask::${INPUT_SESSION_PASSWORD_VALUE}"
fi
write_env "INPUT_PUBLIC_HEALTH_CHECK" "${INPUT_PUBLIC_HEALTH_CHECK_VALUE}"
write_env "KEEPALIVE_SECONDS" "${KEEPALIVE_SECONDS_VALUE}"
write_env "BUILD_WORKFLOW" "${BUILD_WORKFLOW_VALUE}"
Expand Down Expand Up @@ -186,6 +207,12 @@ runs:

echo "SIMDECK_STREAM_PROFILE=${stream_profile}" >> "${GITHUB_ENV}"
echo "SIMDECK_PUBLIC_HEALTH_CHECK=${public_health_check}" >> "${GITHUB_ENV}"
if [[ -n "${SIMDECK_SESSION_PASSWORD:-}" ]]; then
if [[ "${SIMDECK_PROXY_LINKS}" != "true" || -z "${SIMDECK_CI_PROXY_URL:-}" ]]; then
echo "session_password requires proxy_links: true and a non-empty ci_proxy_url." >&2
exit 1
fi
fi
echo "Stream profile: ${stream_profile}"

- name: Install Android SDK packages
Expand Down Expand Up @@ -338,8 +365,7 @@ runs:
SIMDECK_REALTIME_MIN_BITRATE="${stream_min_bitrate}" \
SIMDECK_REALTIME_BITS_PER_PIXEL="${stream_bits_per_pixel}" \
SIMDECK_LOCAL_STREAM_FPS="${stream_fps}" \
simdeck daemon run \
--project-root "${GITHUB_WORKSPACE}" \
simdeck service run \
--metadata-path "${metadata_path}" \
--port "${SIMDECK_PORT}" \
--bind 127.0.0.1 \
Expand Down Expand Up @@ -391,6 +417,72 @@ runs:

echo "url=${tunnel_url}" >> "${GITHUB_OUTPUT}"
echo "access_token=${access_token}" >> "${GITHUB_OUTPUT}"
public_url="${tunnel_url}?simdeckToken=${access_token}"
ci_proxy_url="${SIMDECK_CI_PROXY_URL%/}"
if [[ "${SIMDECK_PROXY_LINKS}" == "true" && -n "${ci_proxy_url}" ]]; then
encoded_redirect="$(SIMDECK_TUNNEL_URL="${tunnel_url}" \
SIMDECK_ACCESS_TOKEN="${access_token}" \
SIMDECK_SESSION_PASSWORD="${SIMDECK_SESSION_PASSWORD:-}" \
SIMDECK_PLATFORM="android" \
SIMDECK_REPO="${REPO}" \
SIMDECK_PR_NUMBER="${PR_NUMBER}" \
SIMDECK_RUN_ID="${GITHUB_RUN_ID}" \
SIMDECK_KEEPALIVE_SECONDS="${KEEPALIVE_SECONDS}" \
node --input-type=module <<'NODE'
import { webcrypto } from "node:crypto";

const env = process.env;
const encoder = new TextEncoder();
const base64url = (bytes) =>
Buffer.from(bytes).toString("base64url");
const keepalive = Number(env.SIMDECK_KEEPALIVE_SECONDS || "1800");
const payload = {
v: 1,
upstream: env.SIMDECK_TUNNEL_URL,
platform: env.SIMDECK_PLATFORM,
repo: env.SIMDECK_REPO,
pr: env.SIMDECK_PR_NUMBER,
runId: env.SIMDECK_RUN_ID,
expiresAt: new Date(Date.now() + (keepalive + 600) * 1000).toISOString(),
};

const password = env.SIMDECK_SESSION_PASSWORD || "";
const token = env.SIMDECK_ACCESS_TOKEN || "";
if (password) {
const salt = webcrypto.getRandomValues(new Uint8Array(16));
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const passwordBytes = encoder.encode(password);
const material = new Uint8Array(passwordBytes.length + salt.length + 1);
material.set(passwordBytes);
material[passwordBytes.length] = 0;
material.set(salt, passwordBytes.length + 1);
const digest = await webcrypto.subtle.digest("SHA-256", material);
const key = await webcrypto.subtle.importKey(
"raw",
digest,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"],
);
const ciphertext = new Uint8Array(
await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(token)),
);
payload.tokenCipher = {
algorithm: "SHA256-SALTED+A256GCM",
ciphertext: base64url(ciphertext),
iv: base64url(iv),
salt: base64url(salt),
};
} else {
payload.token = token;
}

process.stdout.write(base64url(Buffer.from(JSON.stringify(payload))));
NODE
)"
public_url="${ci_proxy_url}?redirect=${encoded_redirect}"
fi
echo "public_url=${public_url}" >> "${GITHUB_OUTPUT}"

public_health_url="${tunnel_url}/api/health?simdeckToken=${access_token}"
tunnel_host="${tunnel_url#https://}"
Expand Down Expand Up @@ -577,7 +669,7 @@ runs:
fi

cat > comment.md <<'EOF'
__MENTION__SimDeck Android session is ready: [Open SimDeck](${{ steps.stream.outputs.url }}?simdeckToken=${{ steps.stream.outputs.access_token }}&device=${{ steps.emulator.outputs.udid }})
__MENTION__SimDeck Android session is ready: [Open SimDeck](${{ steps.stream.outputs.public_url }}&device=${{ steps.emulator.outputs.udid }})

The selected emulator is booted and the PR APK will launch here once its build artifact is installed.

Expand Down Expand Up @@ -699,7 +791,7 @@ runs:
shell: bash
run: |
cat > comment.md <<'EOF'
SimDeck Android session is ready: [Open SimDeck](${{ steps.stream.outputs.url }}?simdeckToken=${{ steps.stream.outputs.access_token }}&device=${{ steps.android.outputs.udid }})
SimDeck Android session is ready: [Open SimDeck](${{ steps.stream.outputs.public_url }}&device=${{ steps.android.outputs.udid }})

App: `${{ steps.android.outputs.package_name }}`
Commit: `${{ steps.pr.outputs.sha }}`
Expand Down
Loading
Loading