Skip to content

Commit f3e8aa4

Browse files
committed
chore: merge main into simdeck-ios
2 parents aafb748 + 03b8018 commit f3e8aa4

44 files changed

Lines changed: 1820 additions & 52 deletions

Some content is hidden

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

.codex/local-environment.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version = 1
2+
name = "SimDeck"
3+
4+
[setup]
5+
script = '''
6+
cd "$CODEX_WORKTREE_PATH"
7+
npm run codex:setup
8+
'''
9+
10+
[cleanup]
11+
script = '''
12+
cd "$CODEX_WORKTREE_PATH"
13+
npm run codex:cache:save
14+
'''
15+
16+
[[actions]]
17+
name = "Build and Restart Daemon"
18+
icon = "run"
19+
command = '''
20+
cd "$CODEX_WORKTREE_PATH"
21+
npm run codex:run
22+
'''
23+
platform = "darwin"

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ npm run package:vscode
102102

103103
This now builds the Rust server in `server/` and copies the resulting binary to `build/simdeck`.
104104

105+
Codex worktrees can use the checked-in local environment config at
106+
`.codex/local-environment.toml`. Its setup runs `npm run codex:setup`, which
107+
hydrates root/client `node_modules` and `server/target` from the shared cache
108+
under `~/.cache/simdeck/codex-worktree-cache` or from another SimDeck checkout
109+
before falling back to `npm ci` for missing package installs and ensuring
110+
Homebrew `pkgconf`/`x264` are available for native builds. Its Run action
111+
executes `npm run codex:run`, which builds the CLI and client, saves fresh
112+
caches, and restarts the workspace-local daemon.
113+
105114
Run the local daemon:
106115

107116
```sh
@@ -137,6 +146,8 @@ Useful direct commands:
137146
./build/simdeck pasteboard set <udid> "hello"
138147
./build/simdeck pasteboard get <udid>
139148
./build/simdeck screenshot <udid> --output screen.png
149+
./build/simdeck screenshot <udid> --with-bezel --output screen-bezel.png
150+
./build/simdeck record <udid> --seconds 5 --output screen-recording.mp4
140151
./build/simdeck describe <udid>
141152
./build/simdeck tap <udid> 120 240
142153
./build/simdeck tap <udid> --label "Continue" --wait-timeout-ms 5000

CONTRIBUTING.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,40 @@ npx skills add NativeScript/SimDeck --skill simdeck -a codex -g
138138

139139
The npm postinstall message also prints this command after a global install.
140140

141+
## Codex local worktrees
142+
143+
This repo includes a Codex local environment at
144+
`.codex/local-environment.toml`. Use it when creating Codex worktrees for
145+
SimDeck. The setup script runs:
146+
147+
```sh
148+
npm run codex:setup
149+
```
150+
151+
That hydrates the root `node_modules`, `client/node_modules`, and
152+
`server/target` from `~/.cache/simdeck/codex-worktree-cache` or a matching
153+
existing SimDeck checkout. If either `node_modules` directory is still missing,
154+
it falls back to `npm ci` for that package so lockfiles stay unchanged. On
155+
macOS it also ensures the Homebrew `pkgconf` and `x264` packages are available
156+
for the native Rust build. Set
157+
`SIMDECK_CODEX_SKIP_BREW=1` if you want setup to report missing Homebrew
158+
packages instead of installing them.
159+
160+
The cleanup script saves fresh caches with:
161+
162+
```sh
163+
npm run codex:cache:save
164+
```
165+
166+
The environment also exposes a **Build and Restart Daemon** Run action:
167+
168+
```sh
169+
npm run codex:run
170+
```
171+
172+
It builds the Rust CLI and React client, saves the refreshed caches, and runs
173+
`./build/simdeck daemon restart` for the current workspace.
174+
141175
## Releasing
142176

143177
Releases are published from the `Release` GitHub Actions workflow at

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ simdeck toggle-appearance <udid>
141141
simdeck pasteboard set <udid> "hello"
142142
simdeck pasteboard get <udid>
143143
simdeck screenshot <udid> --output screen.png
144+
simdeck screenshot <udid> --with-bezel --output screen-bezel.png
145+
simdeck record <udid> --seconds 5 --output screen-recording.mp4
144146
simdeck stream <udid> --frames 120 > stream.h264
145147
simdeck describe <udid>
146148
simdeck describe <udid> --format agent --max-depth 4
@@ -217,6 +219,8 @@ try {
217219
await sim.tap(0.5, 0.5);
218220
await sim.waitFor({ label: "Continue" });
219221
await sim.screenshot();
222+
await sim.screenshot({ withBezel: true });
223+
await sim.record({ seconds: 5 });
220224
} finally {
221225
sim.close();
222226
}

actions/run-ios-comment-session/action.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,58 @@ runs:
545545
fi
546546
date +%s > /tmp/sim-boot-end
547547
548+
- name: Wait for public SimDeck iOS session access
549+
shell: bash
550+
run: |
551+
set -euo pipefail
552+
553+
udid="${SIMULATOR_UDID}"
554+
public_simulators_url="${{ steps.stream.outputs.url }}/api/simulators?simdeckToken=${{ steps.stream.outputs.access_token }}"
555+
tunnel_host="${{ steps.stream.outputs.url }}"
556+
tunnel_host="${tunnel_host#https://}"
557+
tunnel_host="${tunnel_host%%/*}"
558+
559+
fetch_public_simulators() {
560+
curl -fsS "${public_simulators_url}" -o public-simulators.json ||
561+
curl -fsS --resolve "${tunnel_host}:443:104.16.230.132" "${public_simulators_url}" -o public-simulators.json ||
562+
curl -fsS --resolve "${tunnel_host}:443:104.16.231.132" "${public_simulators_url}" -o public-simulators.json
563+
}
564+
565+
verify_selected_simulator() {
566+
SIMDECK_READY_UDID="${udid}" python3 - <<'PY'
567+
import json
568+
import os
569+
570+
with open("public-simulators.json", "r", encoding="utf-8") as handle:
571+
data = json.load(handle)
572+
573+
udid = os.environ["SIMDECK_READY_UDID"]
574+
simulators = data.get("simulators", data if isinstance(data, list) else [])
575+
for simulator in simulators:
576+
if simulator.get("udid") == udid and simulator.get("isBooted") is True:
577+
raise SystemExit(0)
578+
579+
raise SystemExit(1)
580+
PY
581+
}
582+
583+
for attempt in {1..120}; do
584+
if fetch_public_simulators && verify_selected_simulator; then
585+
echo "Public SimDeck URL lists booted simulator ${udid}."
586+
exit 0
587+
fi
588+
echo "Waiting for public SimDeck simulator list (${attempt}/120)."
589+
sleep 1
590+
done
591+
592+
echo "SimDeck public simulator list did not become accessible for ${udid}" >&2
593+
if [[ -f public-simulators.json ]]; then
594+
cat public-simulators.json >&2 || true
595+
fi
596+
cat cloudflared.log >&2 || true
597+
cat simdeck-daemon.log >&2 || true
598+
exit 1
599+
548600
- name: Update status comment with booted simulator URL
549601
shell: bash
550602
run: |

cli/XCWChromeRenderer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
1515
error:(NSError * _Nullable * _Nullable)error;
1616
+ (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
1717
error:(NSError * _Nullable * _Nullable)error;
18+
+ (nullable NSData *)screenshotPNGDataForDeviceName:(NSString *)deviceName
19+
screenPNGData:(NSData *)screenPNGData
20+
error:(NSError * _Nullable * _Nullable)error;
1821
+ (nullable NSDictionary<NSString *, id> *)profileForDeviceName:(NSString *)deviceName
1922
error:(NSError * _Nullable * _Nullable)error;
2023

cli/XCWChromeRenderer.m

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,111 @@ + (nullable NSData *)screenMaskPNGDataForDeviceName:(NSString *)deviceName
249249
return [self PNGDataForPDFAtPath:maskPath scale:1.0 error:error];
250250
}
251251

252+
+ (nullable NSData *)screenshotPNGDataForDeviceName:(NSString *)deviceName
253+
screenPNGData:(NSData *)screenPNGData
254+
error:(NSError * _Nullable __autoreleasing *)error {
255+
NSDictionary *profile = [self profileForDeviceName:deviceName error:error];
256+
if (profile == nil) {
257+
return nil;
258+
}
259+
260+
NSData *chromePNGData = [self PNGDataForDeviceName:deviceName includeButtons:YES error:error];
261+
if (chromePNGData == nil) {
262+
return nil;
263+
}
264+
265+
NSImage *screenImage = [[NSImage alloc] initWithData:screenPNGData];
266+
NSImage *chromeImage = [[NSImage alloc] initWithData:chromePNGData];
267+
if (screenImage == nil || chromeImage == nil) {
268+
if (error != NULL) {
269+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
270+
code:15
271+
userInfo:@{
272+
NSLocalizedDescriptionKey: @"Unable to decode simulator screenshot or chrome PNG data.",
273+
}];
274+
}
275+
return nil;
276+
}
277+
278+
CGFloat scale = 3.0;
279+
CGFloat totalWidth = [self numberValue:profile[@"totalWidth"]];
280+
CGFloat totalHeight = [self numberValue:profile[@"totalHeight"]];
281+
CGFloat screenX = [self numberValue:profile[@"screenX"]];
282+
CGFloat screenY = [self numberValue:profile[@"screenY"]];
283+
CGFloat screenWidth = [self numberValue:profile[@"screenWidth"]];
284+
CGFloat screenHeight = [self numberValue:profile[@"screenHeight"]];
285+
NSInteger pixelWidth = MAX((NSInteger)ceil(totalWidth * scale), 1);
286+
NSInteger pixelHeight = MAX((NSInteger)ceil(totalHeight * scale), 1);
287+
if (totalWidth <= 0.0 || totalHeight <= 0.0 || screenWidth <= 0.0 || screenHeight <= 0.0) {
288+
if (error != NULL) {
289+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
290+
code:16
291+
userInfo:@{
292+
NSLocalizedDescriptionKey: @"Device chrome profile did not include usable screenshot geometry.",
293+
}];
294+
}
295+
return nil;
296+
}
297+
298+
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
299+
pixelsWide:pixelWidth
300+
pixelsHigh:pixelHeight
301+
bitsPerSample:8
302+
samplesPerPixel:4
303+
hasAlpha:YES
304+
isPlanar:NO
305+
colorSpaceName:NSDeviceRGBColorSpace
306+
bytesPerRow:0
307+
bitsPerPixel:32];
308+
if (bitmap == nil) {
309+
if (error != NULL) {
310+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
311+
code:17
312+
userInfo:@{
313+
NSLocalizedDescriptionKey: @"Unable to create a bitmap for bezeled screenshot rendering.",
314+
}];
315+
}
316+
return nil;
317+
}
318+
319+
NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
320+
[NSGraphicsContext saveGraphicsState];
321+
[NSGraphicsContext setCurrentContext:graphicsContext];
322+
graphicsContext.imageInterpolation = NSImageInterpolationHigh;
323+
NSRect outputRect = NSMakeRect(0.0, 0.0, pixelWidth, pixelHeight);
324+
[[NSColor clearColor] setFill];
325+
NSRectFillUsingOperation(outputRect, NSCompositingOperationClear);
326+
327+
NSRect screenRect = NSMakeRect(screenX * scale,
328+
pixelHeight - ((screenY + screenHeight) * scale),
329+
screenWidth * scale,
330+
screenHeight * scale);
331+
NSDictionary *hints = @{ NSImageHintInterpolation: @(NSImageInterpolationHigh) };
332+
[screenImage drawInRect:screenRect
333+
fromRect:NSZeroRect
334+
operation:NSCompositingOperationSourceOver
335+
fraction:1.0
336+
respectFlipped:NO
337+
hints:hints];
338+
[chromeImage drawInRect:outputRect
339+
fromRect:NSZeroRect
340+
operation:NSCompositingOperationSourceOver
341+
fraction:1.0
342+
respectFlipped:NO
343+
hints:hints];
344+
[NSGraphicsContext restoreGraphicsState];
345+
346+
NSData *pngData = [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
347+
if (pngData.length == 0 && error != NULL) {
348+
*error = [NSError errorWithDomain:XCWChromeRendererErrorDomain
349+
code:18
350+
userInfo:@{
351+
NSLocalizedDescriptionKey: @"Unable to encode bezeled simulator screenshot PNG.",
352+
}];
353+
}
354+
return pngData.length > 0 ? pngData : nil;
355+
}
356+
252357
+ (nullable NSDictionary<NSString *, id> *)profileForChromeInfo:(NSDictionary *)chromeInfo
253358
error:(NSError * _Nullable __autoreleasing *)error {
254359
NSDictionary *plist = chromeInfo[@"plist"];

cli/XCWProcessRunner.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ NS_ASSUME_NONNULL_BEGIN
3131
timeoutSec:(NSTimeInterval)timeoutSec
3232
error:(NSError * _Nullable * _Nullable)error;
3333

34+
+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
35+
arguments:(NSArray<NSString *> *)arguments
36+
inputData:(nullable NSData *)inputData
37+
timeoutSec:(NSTimeInterval)timeoutSec
38+
timeoutSignal:(int)timeoutSignal
39+
error:(NSError * _Nullable * _Nullable)error;
40+
3441
@end
3542

3643
NS_ASSUME_NONNULL_END

cli/XCWProcessRunner.m

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
119119
inputData:(NSData *)inputData
120120
timeoutSec:(NSTimeInterval)timeoutSec
121121
error:(NSError * _Nullable __autoreleasing *)error {
122+
return [self runLaunchPath:launchPath
123+
arguments:arguments
124+
inputData:inputData
125+
timeoutSec:timeoutSec
126+
timeoutSignal:SIGTERM
127+
error:error];
128+
}
129+
130+
+ (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
131+
arguments:(NSArray<NSString *> *)arguments
132+
inputData:(NSData *)inputData
133+
timeoutSec:(NSTimeInterval)timeoutSec
134+
timeoutSignal:(int)timeoutSignal
135+
error:(NSError * _Nullable __autoreleasing *)error {
122136
int stdoutFD = -1;
123137
int stderrFD = -1;
124138
int stdinPipe[2] = { -1, -1 };
@@ -244,8 +258,10 @@ + (XCWProcessResult *)runLaunchPath:(NSString *)launchPath
244258
}
245259
if (hasTimeout && [deadline timeIntervalSinceNow] <= 0) {
246260
timedOut = YES;
247-
kill(pid, SIGTERM);
248-
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:2.0];
261+
int signalToSend = timeoutSignal > 0 ? timeoutSignal : SIGTERM;
262+
kill(pid, signalToSend);
263+
NSTimeInterval graceSeconds = signalToSend == SIGINT ? 10.0 : 2.0;
264+
NSDate *killDeadline = [NSDate dateWithTimeIntervalSinceNow:graceSeconds];
249265
do {
250266
waitResult = waitpid(pid, &waitStatus, WNOHANG);
251267
if (waitResult == pid || (waitResult < 0 && errno != EINTR)) {

cli/XCWSimctl.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ NS_ASSUME_NONNULL_BEGIN
1919
- (BOOL)openURL:(NSString *)urlString simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
2020
- (BOOL)launchBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
2121
- (nullable NSData *)screenshotPNGForSimulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
22+
- (nullable NSData *)screenshotPNGForSimulatorUDID:(NSString *)udid
23+
includeBezel:(BOOL)includeBezel
24+
error:(NSError * _Nullable * _Nullable)error;
25+
- (nullable NSData *)screenRecordingMP4ForSimulatorUDID:(NSString *)udid
26+
durationSeconds:(NSTimeInterval)durationSeconds
27+
error:(NSError * _Nullable * _Nullable)error;
2228
- (BOOL)eraseSimulatorWithUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
2329
- (BOOL)installAppAtPath:(NSString *)appPath simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;
2430
- (BOOL)uninstallBundleID:(NSString *)bundleID simulatorUDID:(NSString *)udid error:(NSError * _Nullable * _Nullable)error;

0 commit comments

Comments
 (0)