Skip to content

Commit a47ce0d

Browse files
committed
Add npm packaging and service support
1 parent d3ee75b commit a47ce0d

46 files changed

Lines changed: 985 additions & 301 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
ci:
11+
runs-on: macos-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: npm
20+
cache-dependency-path: |
21+
package-lock.json
22+
client/package-lock.json
23+
24+
- uses: dtolnay/rust-toolchain@stable
25+
with:
26+
components: rustfmt, clippy
27+
28+
- name: Install root dependencies
29+
run: npm ci
30+
31+
- name: Install client dependencies
32+
run: npm ci --prefix client
33+
34+
- name: Lint, build, and test
35+
run: npm run ci

‎.prettierignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.claude
2+
.git
3+
.vscode
4+
build
5+
client/dist
6+
node_modules
7+
server/target

‎README.md‎

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,26 @@
1919
./scripts/build-cli.sh
2020
```
2121

22+
## Install
23+
24+
Install the published CLI globally:
25+
26+
```sh
27+
npm install -g xcode-canvas-web
28+
```
29+
30+
Install the current local checkout globally from source:
31+
32+
```sh
33+
npm install -g .
34+
```
35+
36+
After a global install, use the `xcode-canvas-web` command directly. From a local checkout, you can also run `./build/xcode-canvas-web`.
37+
2238
## Run
2339

2440
```sh
25-
./build/xcode-canvas-web serve --port 4310
41+
xcode-canvas-web serve --port 4310
2642
```
2743

2844
Then open [http://127.0.0.1:4310](http://127.0.0.1:4310).
@@ -31,44 +47,32 @@ The Rust server exposes HTTP on the requested port and WebTransport on `port + 1
3147
The browser bootstrap comes from `GET /api/health`, which returns the WebTransport URL template,
3248
certificate hash, and packet version needed by the client.
3349

34-
To expose the server on your LAN, bind on all interfaces and advertise a host
35-
or IP that the browser can actually reach and validate in the self-signed
36-
certificate:
50+
## Service
51+
52+
Enable the per-user background service with `launchd`:
3753

3854
```sh
39-
./build/xcode-canvas-web serve --port 4310 --bind 0.0.0.0 --advertise-host 192.168.1.50
55+
xcode-canvas-web service on --port 4310
4056
```
4157

42-
Then open `http://192.168.1.50:4310` from another device on the same network.
58+
Disable it:
59+
60+
```sh
61+
xcode-canvas-web service off
62+
```
4363

4464
## CLI
4565

4666
```sh
47-
./build/xcode-canvas-web list
48-
./build/xcode-canvas-web boot <udid>
49-
./build/xcode-canvas-web shutdown <udid>
50-
./build/xcode-canvas-web open-url <udid> https://example.com
51-
./build/xcode-canvas-web launch <udid> com.apple.Preferences
67+
xcode-canvas-web list
68+
xcode-canvas-web boot <udid>
69+
xcode-canvas-web shutdown <udid>
70+
xcode-canvas-web open-url <udid> https://example.com
71+
xcode-canvas-web launch <udid> com.apple.Preferences
5272
```
5373

54-
## HTTP API
55-
56-
- `GET /api/health`
57-
- `GET /api/metrics`
58-
- `GET /api/simulators`
59-
- `POST /api/simulators/:udid/boot`
60-
- `POST /api/simulators/:udid/shutdown`
61-
- `POST /api/simulators/:udid/open-url`
62-
- `POST /api/simulators/:udid/launch`
63-
- `POST /api/simulators/:udid/touch`
64-
- `POST /api/simulators/:udid/key`
65-
- `POST /api/simulators/:udid/home`
66-
- `POST /api/simulators/:udid/rotate-right`
67-
- `GET /api/simulators/:udid/chrome-profile`
68-
- `GET /api/simulators/:udid/chrome.png`
69-
70-
## Status
71-
72-
The live browser preview now comes from the vendored private display bridge rather than `simctl io screenshot`.
73-
The Rust server owns REST routing, session orchestration, metrics, static file serving, and the WebTransport video path.
74-
The native layer owns simulator lookup/boot, chrome rendering, HID input injection, and hardware H.264 encode.
74+
## License
75+
76+
Copyright 2026 Dj
77+
78+
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).

‎bin/xcode-canvas-web.mjs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env node
2+
3+
import { spawnSync } from "node:child_process";
4+
import { existsSync } from "node:fs";
5+
import path from "node:path";
6+
import { fileURLToPath } from "node:url";
7+
8+
const packageRoot = path.resolve(
9+
path.dirname(fileURLToPath(import.meta.url)),
10+
"..",
11+
);
12+
const binaryPath = path.join(packageRoot, "build", "xcode-canvas-web-bin");
13+
14+
if (!existsSync(binaryPath)) {
15+
console.error(
16+
"xcode-canvas-web is not built yet. Reinstall or rebuild the package so the native CLI is available.",
17+
);
18+
process.exit(1);
19+
}
20+
21+
const result = spawnSync(binaryPath, process.argv.slice(2), {
22+
cwd: process.cwd(),
23+
stdio: "inherit",
24+
});
25+
26+
if (result.error) {
27+
console.error(result.error.message);
28+
process.exit(1);
29+
}
30+
31+
process.exit(result.status ?? 1);

‎cli/DFPrivateSimulatorDisplayBridge.m‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -415,14 +415,16 @@ static void DFTrieDescend(DFTrieContext *ctx, const uint8_t *node, size_t nameLe
415415
}
416416

417417
static id DFDigitizerDelegateGetter(id self, SEL _cmd) {
418+
(void)_cmd;
418419
return objc_getAssociatedObject(self, DFDigitizerDelegateAssociationKey);
419420
}
420421

421422
static id DFDigitizerWakeDelegateGetter(id self, SEL _cmd) {
423+
(void)_cmd;
422424
return objc_getAssociatedObject(self, DFDigitizerWakeDelegateAssociationKey);
423425
}
424426

425-
static Class DFEnsureDigitizerProxyClass(Class baseClass) {
427+
static Class __attribute__((unused)) DFEnsureDigitizerProxyClass(Class baseClass) {
426428
if (baseClass == Nil) {
427429
return Nil;
428430
}
@@ -505,7 +507,7 @@ __asm__ volatile(
505507
return result;
506508
}
507509

508-
static id DFCallSwiftSelfGetter(id selfObject, const char *symbolName) {
510+
static id __attribute__((unused)) DFCallSwiftSelfGetter(id selfObject, const char *symbolName) {
509511
if (selfObject == nil) return nil;
510512
void *function = dlsym(RTLD_DEFAULT, symbolName);
511513
if (function == NULL) {
@@ -541,6 +543,7 @@ static id DFCallSwiftSelfGetterByPattern(id selfObject, const char *prefix, cons
541543
NSMutableDictionary<NSNumber *, id> *unwrapped = [NSMutableDictionary dictionaryWithCapacity:[(NSDictionary *)screens count]];
542544
SEL screenSelector = sel_registerName("screen");
543545
[(NSDictionary *)screens enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
546+
(void)stop;
544547
if (![key isKindOfClass:[NSNumber class]] || value == nil) {
545548
return;
546549
}
@@ -556,7 +559,7 @@ static id DFCallSwiftSelfGetterByPattern(id selfObject, const char *prefix, cons
556559
return unwrapped;
557560
}
558561

559-
static uint32_t DFCallSwiftSelfGetterU32(id selfObject, const char *symbolName) {
562+
static uint32_t __attribute__((unused)) DFCallSwiftSelfGetterU32(id selfObject, const char *symbolName) {
560563
if (selfObject == nil) {
561564
return 0;
562565
}
@@ -578,7 +581,7 @@ __asm__ volatile(
578581
return result;
579582
}
580583

581-
static uintptr_t DFCallSwiftUWordGetter(const char *symbolName) {
584+
static uintptr_t __attribute__((unused)) DFCallSwiftUWordGetter(const char *symbolName) {
582585
if (symbolName == NULL) {
583586
return 0;
584587
}
@@ -599,7 +602,7 @@ __asm__ volatile(
599602
return result;
600603
}
601604

602-
static BOOL DFCallSwiftVoidMethodWithSelfAndTwoArgs(id selfObject, id firstArgument, const void *secondArgument, const char *symbolName) {
605+
static BOOL __attribute__((unused)) DFCallSwiftVoidMethodWithSelfAndTwoArgs(id selfObject, id firstArgument, const void *secondArgument, const char *symbolName) {
603606
if (selfObject == nil || firstArgument == nil || secondArgument == nil) {
604607
return NO;
605608
}
@@ -637,7 +640,7 @@ __asm__ volatile(
637640
return YES;
638641
}
639642

640-
static BOOL DFCallSwiftVoidMethodWithSelfAndObject(id selfObject, id firstArgument, const char *symbolName) {
643+
static BOOL __attribute__((unused)) DFCallSwiftVoidMethodWithSelfAndObject(id selfObject, id firstArgument, const char *symbolName) {
641644
if (selfObject == nil || firstArgument == nil) return NO;
642645
void *function = dlsym(RTLD_DEFAULT, symbolName);
643646
if (function == NULL) {
@@ -653,7 +656,7 @@ static BOOL DFCallSwiftVoidMethodWithSelfAndObjectByPattern(id selfObject, id fi
653656
return DFCallSwiftVoidMethodWithSelfAndObjectByFunction(selfObject, firstArgument, function);
654657
}
655658

656-
static BOOL DFCallSwiftVoidMethodWithSelfAndUWordAndBool(id selfObject, uintptr_t firstArgument, BOOL secondArgument, const char *symbolName) {
659+
static BOOL __attribute__((unused)) DFCallSwiftVoidMethodWithSelfAndUWordAndBool(id selfObject, uintptr_t firstArgument, BOOL secondArgument, const char *symbolName) {
657660
if (selfObject == nil) {
658661
return NO;
659662
}
@@ -676,7 +679,7 @@ __asm__ volatile(
676679
return YES;
677680
}
678681

679-
static BOOL DFCallSwiftThrowingMethodWithSelfAndObjectAndUWord(id selfObject, id firstArgument, uintptr_t secondArgument, const char *symbolName, NSError **error) {
682+
static BOOL __attribute__((unused)) DFCallSwiftThrowingMethodWithSelfAndObjectAndUWord(id selfObject, id firstArgument, uintptr_t secondArgument, const char *symbolName, NSError **error) {
680683
if (selfObject == nil || firstArgument == nil) {
681684
return NO;
682685
}
@@ -1055,7 +1058,7 @@ __asm__ volatile(
10551058
return YES;
10561059
}
10571060

1058-
static BOOL DFCallSwiftUnitAngleMeasurementGetter(id selfObject, const char *symbolName, DFUnitAngleMeasurement *measurement) {
1061+
static BOOL __attribute__((unused)) DFCallSwiftUnitAngleMeasurementGetter(id selfObject, const char *symbolName, DFUnitAngleMeasurement *measurement) {
10591062
if (selfObject == nil || measurement == NULL) return NO;
10601063
void *function = dlsym(RTLD_DEFAULT, symbolName);
10611064
if (function == NULL) {
@@ -1089,7 +1092,7 @@ __asm__ volatile(
10891092
return YES;
10901093
}
10911094

1092-
static BOOL DFCallSwiftUnitAngleMeasurementSetter(id selfObject, DFUnitAngleMeasurement measurement, const char *symbolName) {
1095+
static BOOL __attribute__((unused)) DFCallSwiftUnitAngleMeasurementSetter(id selfObject, DFUnitAngleMeasurement measurement, const char *symbolName) {
10931096
if (selfObject == nil) return NO;
10941097
void *function = dlsym(RTLD_DEFAULT, symbolName);
10951098
if (function == NULL) {
@@ -2132,13 +2135,15 @@ - (nullable instancetype)initWithUDID:(NSString *)udid error:(NSError * _Nullabl
21322135
_screenAdapterCallbackUUID,
21332136
_callbackQueue,
21342137
^(id simScreen) {
2138+
(void)simScreen;
21352139
__strong typeof(weakSelf) strongSelf = weakSelf;
21362140
if (strongSelf == nil) {
21372141
return;
21382142
}
21392143
[strongSelf updateStatus:@"CoreSimulator screen proxy connected"];
21402144
},
21412145
^(id simScreen) {
2146+
(void)simScreen;
21422147
__strong typeof(weakSelf) strongSelf = weakSelf;
21432148
if (strongSelf == nil) {
21442149
return;
@@ -2248,6 +2253,7 @@ - (nullable instancetype)initWithUDID:(NSString *)udid error:(NSError * _Nullabl
22482253
[strongSelf notifyDelegateOfFrame:strongSelf->_latestPixelBuffer];
22492254
},
22502255
^(id surface, id maskedSurface) {
2256+
(void)maskedSurface;
22512257
__strong typeof(weakSelf) strongSelf = weakSelf;
22522258
if (strongSelf == nil) {
22532259
return;

‎client/index.html‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1.0, user-scalable=no"
8+
/>
69
<meta name="color-scheme" content="light dark" />
710
<title>Xcode Canvas Web</title>
811
</head>

‎client/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"type": "module",
77
"scripts": {
88
"dev": "vite",
9-
"build": "tsc --noEmit && vite build",
9+
"typecheck": "tsc --noEmit",
10+
"build": "npm run typecheck && vite build",
1011
"preview": "vite preview",
1112
"test": "vitest run"
1213
},

‎client/src/api/client.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { API_ROOT } from "../shared/constants";
22

3-
export async function apiRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
3+
export async function apiRequest<T>(
4+
path: string,
5+
options: RequestInit = {},
6+
): Promise<T> {
47
const response = await fetch(`${API_ROOT}${path}`, {
58
headers: { "Content-Type": "application/json" },
6-
...options
9+
...options,
710
});
811

912
const contentType = response.headers.get("content-type") ?? "";

‎client/src/api/controls.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@ import type {
55
OpenUrlPayload,
66
SimulatorMetadata,
77
SimulatorResponse,
8-
TouchPayload
8+
TouchPayload,
99
} from "./types";
1010

1111
async function postSimulatorAction(
1212
udid: string,
1313
action: string,
14-
payload?: KeyPayload | LaunchPayload | OpenUrlPayload | TouchPayload
14+
payload?: KeyPayload | LaunchPayload | OpenUrlPayload | TouchPayload,
1515
): Promise<SimulatorMetadata | null> {
1616
const response = await apiRequest<SimulatorResponse | { ok: boolean }>(
1717
`/api/simulators/${udid}/${action}`,
1818
{
1919
method: "POST",
20-
body: payload ? JSON.stringify(payload) : undefined
21-
}
20+
body: payload ? JSON.stringify(payload) : undefined,
21+
},
2222
);
2323

2424
return "simulator" in response ? response.simulator : null;

‎client/src/api/simulators.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { apiRequest } from "./client";
2-
import type { ChromeProfile, SimulatorMetadata, SimulatorsResponse } from "./types";
2+
import type {
3+
ChromeProfile,
4+
SimulatorMetadata,
5+
SimulatorsResponse,
6+
} from "./types";
37

48
export async function listSimulators(): Promise<SimulatorMetadata[]> {
59
const data = await apiRequest<SimulatorsResponse>("/api/simulators");

0 commit comments

Comments
 (0)