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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ Drive iOS Simulator apps from the CLI, browser, and automated tests on macOS.
npm i -g simdeck@latest
```

After installing the CLI, install the Codex skill so agents know the stable
SimDeck workflow:

```sh
npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
```

For VS Code, install the `nativescript.simdeck` extension to open the simulator
view inside the editor.

## Features

- WebTransport based streaming server in Rust, hardware encoded HVEC/H264 video stream
- Simulator control & inspection using private accessibility APIs
- CoreSimulator chrome asset rendering for device bezels
- NativeScript and React Native runtime inspector plugins, plus a native UIKit inspector framework for other apps
- Project daemon reuse: normal CLI commands automatically start and reuse one warm native host per project.
- Optional macOS LaunchAgent service for an always-on local SimDeck daemon.
- `simdeck/test` for fast JS/TS app tests that can query accessibility state and drive simulator controls.
- Agent [`SKILL.md`](./skills/simdeck/SKILL.md) reference

Expand Down Expand Up @@ -43,6 +54,16 @@ npm install -g .

After a global install, use the `simdeck` command directly. From a local checkout, you can also run `./build/simdeck`.

Install the agent skill with [skills.sh](https://skills.sh/):

```sh
npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
```

The npm postinstall message also prints this command after a global install.
It also recommends `simdeck service on` for always-on local access from agents
and editor integrations.

## Documentation

Full documentation lives at [simdeck.nativescript.org](https://simdeck.nativescript.org/), with guides, the CLI reference, the REST API, the WebTransport video pipeline, and the inspector protocols. The source for the site lives in [`docs/`](docs/) — preview it locally with `npm run docs:dev`.
Expand Down Expand Up @@ -79,6 +100,18 @@ simdeck daemon status
simdeck daemon stop
```

`simdeck daemon` manages the normal per-project warm process. For an always-on
daemon that is available after login, use the macOS user service commands:

```sh
simdeck service on
simdeck service off
```

This uses a LaunchAgent, keeps the server bound to localhost by default, and is
best for agents or editor integrations that should be able to open SimDeck
without first starting a project daemon.

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

```sh
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "vitepress";

const repoName = "SimDeck";
const githubUrl = `https://github.com/DjDeveloperr/${repoName}`;
const githubUrl = `https://github.com/NativeScript/${repoName}`;
const siteUrl = "https://simdeck.nativescript.org";

export default defineConfig({
Expand Down
2 changes: 1 addition & 1 deletion docs/api/health.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Returns a snapshot of every server-side counter and the rolling buffer of client

`client_streams` is a rolling buffer of the most recent reports a client posted to `POST /api/client-stream-stats`. The server keeps the last 48 entries per `(clientId, kind)` pair.

The browser client uses these to render its in-app diagnostics overlay and to size its decoder workers. Every field is optional except `clientId` and `kind`; see [`ClientStreamStats`](https://github.com/DjDeveloperr/SimDeck/blob/main/server/src/metrics/counters.rs) for the full schema.
The browser client uses these to render its in-app diagnostics overlay and to size its decoder workers. Every field is optional except `clientId` and `kind`; see [`ClientStreamStats`](https://github.com/NativeScript/SimDeck/blob/main/server/src/metrics/counters.rs) for the full schema.

## Submitting client stats

Expand Down
20 changes: 20 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ Stop the daemon for the current project:
simdeck daemon stop
```

### `service`

Manage the optional always-on macOS user service. Use `simdeck daemon` for the
normal per-project process; use `simdeck service` when you want a LaunchAgent
that starts after login and stays available.

```sh
simdeck service on [--port 4310] [--bind 127.0.0.1]
[--advertise-host <host>] [--client-root <path>]
[--video-codec hevc|h264|h264-software]
[--access-token <token>]
simdeck service restart [same options as service on]
simdeck service off
```

`service on` installs `~/Library/LaunchAgents/dev.nativescript.simdeck.plist`
and starts a LaunchAgent that serves SimDeck after login. It is intended for
agents and editor integrations that should be able to open the UI without first
starting a project daemon.

### `core-simulator`

Manage Apple's CoreSimulator service layer:
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Optional:
Clone, install dependencies, and build everything:

```sh
git clone https://github.com/DjDeveloperr/SimDeck.git
git clone https://github.com/NativeScript/SimDeck.git
cd simdeck
npm install
npm run build
Expand Down
32 changes: 32 additions & 0 deletions docs/guide/daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ SimDeck runs one warm native host per project. The daemon owns the HTTP API, the

Normal CLI commands start the daemon automatically when they need it. Use `simdeck daemon` only when you want to manage it explicitly.

`simdeck daemon` is project-scoped. `simdeck service` is the optional macOS
LaunchAgent wrapper for users who want an always-on daemon after login.

## Start

```sh
Expand Down Expand Up @@ -69,6 +72,35 @@ simdeck daemon stop

This terminates the daemon for the current project and removes its metadata file from the system temp directory. The next CLI command that needs the daemon starts a fresh one.

## Always-On Service

For agents and editor integrations that should be able to reach SimDeck at any
time after login, use `simdeck service` to install the macOS user service:

```sh
simdeck service on
```

This writes `~/Library/LaunchAgents/dev.nativescript.simdeck.plist`, starts the
server with `launchctl`, and keeps it alive. It binds to `127.0.0.1:4310` by
default and serves the bundled browser client.

Restart it after changing options:

```sh
simdeck service restart --port 4310 --video-codec h264-software
```

Disable it when you do not want a persistent daemon:

```sh
simdeck service off
```

Prefer the project daemon for project-scoped metadata and automatic lifecycle.
Use the service when the priority is easy access from Codex, VS Code, or a
browser at any time.

## CoreSimulator Service Layer

The project daemon is different from Apple's CoreSimulator service. If `simctl` reports stale service state or the live display never produces a first frame, restart Apple's service layer:
Expand Down
29 changes: 28 additions & 1 deletion docs/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,39 @@ This installs the launcher and bundled native binary to your global `node_module
simdeck --help
```

The global install prints the next setup steps:

```sh
simdeck ui --open
npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
simdeck service on
```

Install the `nativescript.simdeck` VS Code extension if you want the simulator
view inside VS Code.

`simdeck service on` is recommended when agents or editor integrations should be
able to reach SimDeck any time after login. It installs a localhost macOS
LaunchAgent and can be removed with `simdeck service off`.

## Install the Codex skill

SimDeck includes an agent skill at `skills/simdeck/SKILL.md`. Install it with
[skills.sh](https://skills.sh/) so Codex can choose the right commands and
inspection loops automatically:

```sh
npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
```

Restart Codex after installing the skill.

## Install from source

Clone the repo and install dependencies:

```sh
git clone https://github.com/DjDeveloperr/SimDeck.git
git clone https://github.com/NativeScript/SimDeck.git
cd simdeck
npm install
```
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ hero:
link: /guide/
- theme: alt
text: View on GitHub
link: https://github.com/DjDeveloperr/SimDeck
link: https://github.com/NativeScript/SimDeck

features:
- icon:
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"LICENSE",
"README.md",
"bin/",
"scripts/postinstall.mjs",
"build/simdeck-bin",
"client/dist/",
"packages/simdeck-test/dist/"
Expand Down Expand Up @@ -52,6 +53,7 @@
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"postinstall": "node scripts/postinstall.mjs",
"prepack": "npm run build:cli && npm run build:client"
},
"devDependencies": {
Expand Down
47 changes: 40 additions & 7 deletions packages/simdeck-test/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export async function connect(options = {}) {
return session;
}
async function startIsolatedDaemon(cliPath, options) {
const port = options.port ?? (await freePort());
const port = options.port ?? (await freePortPair());
const projectRoot = fs.mkdtempSync(
path.join(os.tmpdir(), "simdeck-test-project-"),
);
Expand Down Expand Up @@ -204,12 +204,13 @@ async function startIsolatedDaemon(cliPath, options) {
],
{
cwd: options.projectRoot,
stdio: "ignore",
stdio: ["ignore", "pipe", "pipe"],
},
);
const output = captureChildOutput(child);
const url = `http://127.0.0.1:${port}`;
try {
await waitForHealth(url, child);
await waitForHealth(url, child, output);
} catch (error) {
child.kill();
fs.rmSync(projectRoot, { recursive: true, force: true });
Expand All @@ -225,12 +226,14 @@ async function startIsolatedDaemon(cliPath, options) {
isolatedRoot: projectRoot,
};
}
async function waitForHealth(endpoint, child) {
const deadline = Date.now() + 15_000;
async function waitForHealth(endpoint, child, output) {
const deadline = Date.now() + 60_000;
let lastError;
while (Date.now() < deadline) {
if (child.exitCode !== null) {
throw new Error(`SimDeck isolated daemon exited with ${child.exitCode}`);
throw new Error(
`SimDeck isolated daemon exited with ${child.exitCode}.\n${output()}`,
);
}
try {
await requestJson(endpoint, "GET", "/api/health");
Expand All @@ -241,9 +244,30 @@ async function waitForHealth(endpoint, child) {
}
}
throw new Error(
`Timed out waiting for isolated SimDeck daemon: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
`Timed out waiting for isolated SimDeck daemon: ${lastError instanceof Error ? lastError.message : String(lastError)}\n${output()}`,
);
}
function captureChildOutput(child) {
const chunks = [];
const append = (source, chunk) => {
chunks.push(`[${source}] ${chunk.toString("utf8")}`);
while (chunks.join("").length > 16_384) {
chunks.shift();
}
};
child.stdout?.on("data", (chunk) => append("stdout", chunk));
child.stderr?.on("data", (chunk) => append("stderr", chunk));
return () => chunks.join("").trim();
}
async function freePortPair() {
for (let attempt = 0; attempt < 100; attempt += 1) {
const port = await freePort();
if (port < 65535 && (await portAvailable(port + 1))) {
return port;
}
}
throw new Error("Unable to allocate adjacent free TCP ports.");
}
function freePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
Expand All @@ -260,6 +284,15 @@ function freePort() {
server.on("error", reject);
});
}
function portAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", () => resolve(false));
server.listen(port, "127.0.0.1", () => {
server.close(() => resolve(true));
});
});
}
function runJson(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd,
Expand Down
Loading
Loading