Releases: tddworks/baguette
Release list
baguette v0.1.96
Fixed
serveran away to 9+ GB within minutes of streaming. Reading
framebufferSurfaceis not a local property read: it forwards through
ROCKit to CoreSimulatorService as a synchronous XPC round-trip. Every
framebuffer notification scheduled its ownqueue.async { captureLatest() },
so once that round-trip grew slower than the frame interval — two streams
plus CoreImage scaling and VideoToolbox encoding is enough — captures were
enqueued faster than they drained, and each queued duplicate paid another
round-trip and its own autorelease churn. The result was bimodal: flat for
a minute, then ~50 MB/s until the CoreSimulator connection broke. A heap
taken at the peak was 428,873 autorelease-pool pages (1.76 GB) alongside
24k livexpc_uuid_tand 17kIOSurface.PendingCapturenow coalesces
notifications onto the one already-queued capture — every capture reads the
latest surface, so a duplicate would only refetch the same frame — and
captureLatestdrains its own autorelease pool. Measured over 6 minutes of
streaming, walking and 12 stream restarts: peak 143 MB, versus 1489 MB
before. Noteps rsscannot see this — the pages are dirty and compressed
straight to swap, so RSS reads ~70 MB while the physical footprint is 9 GB.- A slow WebSocket client grew the server without bound. Each encoded
frame chained a newTaskonto the last with no cap and no backpressure,
so any consumer deficit accumulated whole frames indefinitely — a fully
stalled MJPEG client added 586 MB in 60 seconds.FrameBacklogbounds the
pending frames by byte budget, discarding oldest-first while never
dropping the newest avcC description a decoder needs to start, nor the
newest frame. Only the newest description is protected: the encoder
rebuilds its session on every resolution change and emits a fresh one
each time, so protecting all of them would have let a stalled client
hold an unbounded number and the budget would have stopped being a
bound. - A CarPlay pane could leave a live socket nothing tracked. Starting a
CarPlay session awaits its brand-chrome load, and a format swap or a pane
close landing during that await started a second one. The slower start
then mounted onto a canvas the newer had already replaced and overwrote
carplaySessionwithout stopping it — an untracked WebSocket streaming
video at a detached canvas until the page unloaded. Starts now carry a
generation and stand down when overtaken.
Changed
- Companion screens stream in the session's own format. CarPlay and the
paired watch were pinned to MJPEG, so a session set to H.264 still carried
uncapped full JPEGs on its companion sockets. They now follow
currentFormat()like every other surface — one whitelisted accessor,
replacing the two divergent copies of the stored-format read that had let
a format the build no longer speaks reach the socket. The pin dated from a
concern
that a mostly-static CarPlay plane would starve H.264 of an IDR cadence the
guest never produces, butAVCCStreamre-encodes its last surface on an
idle pump for exactly that reason — measured on an idle CarPlay screen,
avcc delivers ~59 fps where MJPEG (which has no idle pump) delivers one
frame per twelve seconds.
What's Changed
- fix(screen): coalesce framebuffer captures so streaming can't run away by @crockalet in #69
Full Changelog: v0.1.95...v0.1.96
baguette v0.1.95
Full Changelog: v0.1.94...v0.1.95
baguette v0.1.94
Fixed
- A new injected dylib silently broke the Homebrew build. Each one needed
its own stanza inbuild.sh, in.gitignore, and in homebrew-core's
hardcoded framework table.VirtualNetwork(0.1.93) updated the first two,
so Homebrew kept installing the committed universal binary and its CI failed
on bothbrew audit("Unexpected universal binaries were found") and
relocation ("Updated load commands do not fit in the header").
Injected/build.shnow loops overInjected/*/build.sh, so a new dylib is
picked up with no list to update anywhere. - Gathering the dylibs under
Injected/broke the formula's source glob.
The formula rebuilds each dylib from<Name>/Sources/*.m, which stopped
matching when the sources moved.clangexits 0 on an empty source
list, emitting a valid, loadable, symbol-less 16KB stub — so 0.1.93 would
have installed camera and motion dylibs that injected nothing and reported
no error, and only the unrelatedbrew auditfailure kept that off users'
machines. The build now fails outright when a dylib exports no symbols.
Changed
- Injected dylibs build fat (arm64 + x86_64) as before, but
BAGUETTE_INJECTED_ARCHSnarrows that to a single host slice for
packagers, and every slice is now linked with
-headerpad_max_install_namesso Homebrew can rewrite the install ID
during relocation.
Removed
- Stale top-level
VirtualCamera/VirtualCamera.dylib, left behind when the
injected dylibs moved underInjected/.
What's Changed
Full Changelog: v0.1.93...v0.1.94
baguette v0.1.93
Added
-
Network conditioning —
baguette network set|clear|status. Makes a
simulator's apps see a worse network than your Mac has: added latency, a
capped downlink, a proportion of requests failing, or hard offline. Named
presets borrow Network Link Conditioner's vocabulary and its figures
(wifi,dsl,lte,3g,edge,very-bad-network,100-loss), so
3gmeans what everyone already means by 3G. NLC itself, and the
dnctl/pfctlrules under it, are system-wide — simulator apps use
the host's stack as the host user, so there is no interface to scope a
rule to and conditioning one simulator that way degrades the whole Mac.
Injecting into the app under test is the only way to scope it, so this
works the way the virtual camera and
motion do. Only apps launched after
network setare conditioned; changing the condition afterwards reaches
a running app without a relaunch.
Which interception mechanism matters was measured before any of it was
designed:+[NSURLProtocol registerClass:]reaches onlyNSURLConnection
andNSURLSession.shared, and against a real React Native app it caught
zero of the app's own requests over 100 seconds. Swizzling
+defaultSessionConfigurationis what reachesfetch, image loading and
REST clients; both ship, and the load banner says which took. Two more
measured facts shape the code: request bodies arrive only as
HTTPBodyStream, which reads once, so a conditioned request is never
retried; and the response is paced as bytes arrive rather than buffered
and replayed, so a 23 MB bundle doesn't sit in memory.
The hazard this is designed against is forgetting it is on — unlike a
wrong camera picture, a throttle reads as "the app is slow" days later. So
plainbaguette networkreports the current condition,network clear
un-conditions apps that are already running, and the browser keeps an
amber dot lit whether or not its card was ever opened. Honest about its
reach: URLSession-shaped traffic only.URLSessionWebSocketTaskgets its
own hooks and takes latency, loss and offline (not bandwidth — an app
cannot observe a partial message). Not conditioned:WKWebViewpage
loads,NWConnection/Network.framework, raw sockets, and realtime SDKs
that open their own socket — Ably'sably-cocoavendors SocketRocket, so
--offlinewill not feel offline to it. See
docs/features/network.md. -
Install plugins from the browser — from bakeries you already trust.
The rail's + now opens a shelf of every trusted bakery, its pinned
commit, and what it offers: an Install button on anything you don't
have, Installed on anything you do, and the rail picks the new plugin
up without a reload. Previously the browser could only preview and hand
you a command to paste.
The trust boundary moved, but only halfway, and the half that matters
stayed put.POST /bakeries/installnames a bakery by its recorded
id, never a URL or a git ref — so a request can only reach a source
already inbakeries.json, at the commit pinned there, and a plugin
that source's own menu lists. Installing writes files baguette later
executes from and the only thing in front of a browser route is a set
of origin heuristics; naming sources by recorded id is what keeps the
blast radius of a wrong one at "installs from a repo you already
vetted" instead of "clones anything onto your disk". A refusal never
echoes the id it was handed back into the page.
Trusting a new source is still not something a page can do —
baguette bakery addstays a terminal act, because a modal button
isn't consent (the page sets the flag it then checks) and trust is the
decision that actually matters. Preview still ends by handing you the
command. The decision isInstallDecisioninDomain/Bakery/with
every refusal path unit-tested, and installing still only copies
files — nothing runs until you open the plugin's panel.
GET /bakeries.jsonnow reports each offer's install state, decided
host-side from what the plugin scan can see rather than from
installed.json(a bundled plugin has no provenance record and must
still read as satisfied). See
docs/features/plugins.md.
Fixed
-
Adding a bakery no longer collides with itself and kills the clone.
GitCheckout.cloneemptied the cache directory and then cloned into it,
leaving the live tree half-written for the tens of seconds a clone takes.
A second clone of the same bakery arriving in that window — the browser's
preview against a terminalbakery add, or one impatient second press of
Preview — deleted the tree the first was still writing into, and git died
on its own vanished temp pack:
fatal: could not open '…/pack/tmp_pack_XXXXXX' for reading: No such file or directory/fatal: fetch-pack: invalid index-pack output.
Each clone now assembles the checkout in a staging directory of its own
beside the destination and moves it in only once it is complete, so
nothing ever deletes a checkout in progress and whoever finishes last
wins the swap. Two consequences worth having on their own: a failed clone
now leaves the checkout you already had instead of a network blip taking
the working copy with it, and a reader never sees the cache directory
mid-delete. The modal also runs one preview at a time — the button
disables while a clone is in flight, and says that a cold one takes a
minute rather than showing a bare "Fetching…". -
The add-a-bakery modal drew behind the device. It mounted into
.right-railsalongside the plugin rail, and.right-railsis
position: fixed— which creates a stacking context whatever its
z-indexsays. So the modal'sz-index: 60was weighed against its
siblings inside the rail rather than against the page, and the device's
z-index: 2screen area a level up painted over it: the phone cut the
dialog in half and the scrim dimmed everything except the thing it was
covering. Page-covering UI now hangs off the view root instead. The
comment on.right-railsclaimed the opposite ("withz-index: auto
this container doesn't create a stacking context") and has been corrected
—position: fixedalone is enough.
What's Changed
- feat(network): per-simulator network conditioning by @crockalet in #66
Full Changelog: v0.1.92...v0.1.93
baguette v0.1.92
Added
-
Motion —
baguette motion start|set|stop, and a walk that drives it.
Makes a simulator's apps readCMMotionActivity(walking, running,
cycling, automotive),CMPedometercounters, andCMMotionManager
samples. All three report unavailable in a stock simulator —
CoreMotion and locationd both gate on a hardware-capability bit derived
from the device's HW type, and a simulated device is an "Unsupported HW
type", so locationd refuses a motion-activity subscription outright. The
runtime even ships a simulation hook
(simulateMotionState:withState:withHint:) that locationd accepts and
that changes nothing, because the availability gate sits upstream of it.
So this works the way the virtual camera does:
by injecting a dylib into the app under test. Only apps launched after
motion startsee anything — dyld inserts at exec time.
Turn it on in the browser's Location card and the walk joystick and
route speeds it already posts classify the activity, so the preset you
picked (Walk 1.4 · Cycle 6 · Drive 13.4) is what your app observes;
pinning a point parks it as stationary, which is exactly when locationd
dropscourseto-1. Motion stays opt-in — moving the device never
arms a simulator-wideDYLD_INSERT_LIBRARIESon your behalf.
The ABI notes are the part worth keeping: those{fff}structs pass by
value (a pointer reads zeros and displaces the timestamp),
CMGyroData's initialiser takes degrees while its property returns
radians,CMDeviceMotion's quaternion is storedw,x,y,zagainst a
publicx,y,z,w, itsgravityis derived from attitude rather than set,
it ignores its owntimestamp:argument, andCMMotionActivitybuilt by
poking ivars reads back fine and then crashes in-description. A
load-time self-check verifies each surface and skips hooking any that
fails, so a future iOS leaves apps seeing the platform's honest
"unavailable" rather than fabricated garbage. Floor counting and the
magnetometer are refused on purpose. See
docs/features/motion.md. -
One output size for every capture —
--size appstore-6.9, and the
same words in the UI. Screenshots and recordings, 2D and 3D, browser
and CLI, now share one vocabulary: the App Store submission sizes
(appstore-6.9,appstore-6.5,appstore-ipad-13), the common ratios
(square,16:9,9:16,4:3,4:5), plus1920x1080and any bare
W:H. Pick "App Store 6.9″" from the toolbar chip once and reproduce
exactly those pixels in CI with--size appstore-6.9.A ratio grows rather than crops:
squareon a 1290 × 2796 phone
gives a 2796 × 2796 canvas with the whole phone centred, not a
1290 × 1290 cut through the middle of the screen. Cropping the device
out of a marketing shot is the one thing nobody asking for a square
wanted.--fit(contain/cover/stretch) and--background
say what fills the rest. Unknown sizes are rejected — baguette never
substitutes a nearby one. -
baguette record— video straight from the CLI. A booted simulator,
an--output, and either--durationorCtrl-C; the file is flushed
and playable either way. Takes--size/--fit/--fps/
--bitrate, writes.mp4or.mov(the extension picks the
container).docs/features/recording.mdhas argued for a while that server-side
recording was tried and rejected, and that argument still stands for
the live stream — a recorder attaching mid-stream never sees the
SPS/PPS the encoder emitted on its first IDR, and an N+1th
VideoToolbox session stutters every farm tile. Neither applies to a
standalone CLI run: it owns the encode from frame one and has no
competing viewer. So the design that was wrong as a passenger is the
right one on its own, and it is wired into nothing — no route, no WS
verb. -
PNG screenshots, and a bezelled one.
baguette screenshot --format png(inferred from a.png --output, so-o shot.pngcan
never quietly hold JPEG bytes), plusGET …/screenshot.pngand
GET …/screenshot-bezel.png— the frame composited inside its
DeviceKit chrome, which previously meant opening a browser and
cropping. All three routes take?size=&fit=&background=;
screenshot.jpgwith no new parameters returns byte-identical output
to before. -
A size chip on every capture surface, and a Record button where
there wasn't one. The focus-mode view had only ever had Screenshot; it
now records too, in both 2D and 3D — dropping the bezel in 3D, since
the rendered frame already contains a device. The legacy stream
sidebar and the device-farm focus pane get the same chip, each
remembering its own selection, and it drives Capture and Record alike:
a screenshot and a clip taken a second apart come out at the same
dimensions. Saved files are named for what they are —
…-appstore-6.9-1290x2796.png. -
The browser's 3D export is now lossless. It used to save
toDataURL()of the decoded video canvas — a ~960 px H.264 or MJPEG
frame. It now asks the server to re-render the same pose at the picked
size, soappstore-6.9really is 1290 × 2796 instead of an upscale of
a video still.render-3d --sizeand the route's"size"field take
preset names as well as literal pixels, and the live 3D stream accepts
size=. -
Deep links —
baguette openurl <url>andbaguette schemes. Opens a link
on a booted simulator, and lists the URL schemes its apps registered (ranked:
an app's own scheme before its reverse-DNS andexp+aliases). Unlike
simctl openurl/idb open/ Maestro,openurlwarns thathttps://lands
in Safari rather than your app — the simulator doesn't resolve associated
domains, so dispatch succeeds and your app never comes up. Schemes need two
reads:
simctl listappsgives the roster and each app'sPathbut not
CFBundleURLTypes, so those come from<Path>/Info.plist. Over HTTP as
POST /simulators/:udid/openurlandGET /simulators/:udid/schemes.json,
reachable by a plugin holding the newopen-urlcapability — which is apart
fromapps, since this only launches software that is already installed.
Seedocs/features/deep-links.md. -
A deep-link panel, as an installable official plugin. Not in the toolbar —
baguette ships the toolbar, this is a thing you choose:baguette bakery add tddworks/baguette baguette plugin install deeplink
This makes baguette's own repo a bakery. Only
a11ystill ships inside the
binary; everything else maintained alongside baguette is official and
installed on purpose. -
Plugin panels can be operated, not just read. A panel was a report: the
host ran a command and drew the rows. It can now carry a text field
(body.prompt) and tickable rows (body.control— switches, checkboxes,
radios, grouped so one panel can ask several questions). Both invoke the
panel's ownsourcecommand withargs, which is the pathrowAction: "run"
already took, so this adds widgets rather than an execution model — still no
plugin code in the page.The field completes as you type (
Tab/→to accept), remembers the last 25
submissions on↑/↓, filters the rows already on screen, and takes a row's
text on click viarowAction: "fill"— so a list of suggestions behaves like a
URL bar instead of a launcher. History is browser-side: a plugin sees what you
submit, never what you typed before.Ticks are local and batched — no subprocess per tick, and rows the device
hasn't confirmed are drawn pending until the submit returns. Every answer
rebuilds them from what the plugin reported, so a refused setting snaps back.
a11y's display panel uses this (1.2.0) and no longer writes"● Light"/
"○ Dark"into row titles. All of it is additive —apiVersionstays 1.
Changed
-
The plugins rail follows the focus-mode design system. The rail, its
flyout, the panel and the bakery modal were styled by a stylesheet
sim-plugins.jsinjected intodocument.head— and a stylesheet written
beside its module can't see the--nv-*theme tokens, which are defined on
#simNativeView. Every colour in it therefore carried a guessed fallback,
and the guesses were light-theme values: light-slate shadows with no dark
variant, a whitevar(--panel)text field inside a dark glass modal, and an
accent pulled fromsim.html's light-only--accentrather than
--nv-accent. It also invented its own geometry — 34×34 rail buttons at
radius 9 against the toolbar's 30×28 at radius 8, a600 10px/0.06emaccent
heading against the app's700 9.5px/0.10emfaint one, a fourth primary
button, and four 2px accent seams no other surface has.All of it now lives in
sim-native.htmlunder#simNativeView, beside the
panels it sits next to, with no fallbacks — the same arrangement the logs,
status-bar, location and a11y panels already use. The companion-screens rail
moved with it and the two now share every rule they had duplicated. The rails
being separate is the trust signal (seedocs/features/plugins.md); the
plugin rail keeps its accent emblem to say which is which, and drops the
second colour system that was layered on top.
Added
--nv-danger,--nv-warnand--nv-scrimtheme tokens, in both light and
dark. A panel reporting an error previously picked its own red, and the one
it picked was a light-theme#b91c1con a near-black page. Plugin row
severity is now adata-severityattribute the stylesheet colours, rather
than an inline hex the module carries.
What's Changed
- fix(web): style the plugins rail from the focus-mode design system by @hanrw in https://github.com/tddworks...
baguette v0.1.91
Added
- Companion screens rail. A rail on the right edge of
/simulators/<udid>
offering the screens a simulator can show beside its own: its CarPlay /
external display, and the Apple Watch paired with it. A screen the host
doesn't have keeps its slot and says how to get one, an unbooted watch gets a
Boot button, and a missing CarPlay display gets a button that drives
Simulator.app's menus for you. The rail re-probes on focus, since attaching
happens in another app; open panes survive a reload. New routes:GET /simulators/:udid/companion-screens.json,POST /simulators/:udid/carplay-display. See
docs/features/companion-screens.md. - Digital Crown and Side button, under the watch pane. A watch has no bezel
chrome to hang overlay buttons off, so without them the only way out of an app
was to already know the crown exists. Scroll by dragging on the face — the
crown's rotation is a separate HID axis baguette doesn't drive.
Fixed
- Touching an external display pane restarted the simulator. The target came
fromIndigoHIDTargetForScreen, which returns a plausible number no service
has registered — and the CarPlay digitizer had never been created either.
Unregistered targets make the guest throw and take SpringBoard with it, so
targets are constants now and the service is created before use, removed on
teardown, and failed closed if it can't be made. - Opening a device's tab attached a CarPlay display to it. The pane mounted
unconditionally and?display=carplayasks the host to enable CarPlay, so
merely looking at a simulator turned one on. - The CarPlay pane was a black rectangle with no explanation. Availability
came from a name in Connected Screens, but a display can be registered with no
framebuffer behind it. It's the sameDisplay.resolve()the stream performs
now, and a stream that fails to bind renders the error under the pane instead
of logging it to the console. - The pane called itself CarPlay while showing any external display. It binds
the best external, and the External Displays menu offers plain resolutions
too — which on an iOS 27 beta attach and stream while CarPlay attaches nothing. - External displays were bound by area alone. Anything above
800 × 480 × 4
was refused outright, so a 1080p display reported "nothing attached"; with the
bound lifted, a 4K one then out-measured the phone and took the device plane,
putting both planes on the wrong port. The device plane is picked by shape now,
area only as the tie-break, and landscape means strictly wider than tall. - Two-finger gestures from the browser were built on the wrong thread.
IndigoHIDMessageForMouseNSEventreads NSEvent thread-local state, so it must
be built on the main actor. ThePOST …/inputroute always hopped; the stream
socket — the path the browser's pinch and two-finger pan actually ride — did
not, and produced messages the simulator silently drops. - The device rendered at half size when nothing was beside it. The phone was
pinned to46vwat every width for the CarPlay pane's benefit, and the
narrow-window rule meant to relax that lost silently to it. Pane sizing is one
set of variables now, reacting to window width and how many panes are open.
What's Changed
- feat: companion screens rail with carplay and paired watch support by @crockalet in #47
Full Changelog: v0.1.90...v0.1.91
baguette v0.1.90
Added
- Interface settings — appearance, contrast, text size. The three
accessibility-display settings a simulator exposes: light / dark appearance,
Increase Contrast, and content size (Dynamic Type, including the five
"Larger Accessibility Sizes" where layouts actually break).baguette interface appearance|contrast|text-size --udid <UDID> [<value>]— each leaf
reads with no value and sets with one, mirroringsimctl uiitself. Over
HTTP asGET /simulators/:udid/interface.jsonandPOST /simulators/:udid/interface(any subset; answers with the resulting state).
The gotcha worth preserving: a read can answerunknown(device not booted)
orunsupported, and simctl exits 0 for both — they're states to show, not
failures, so reads return them. They're also not instructions, so setting one
is refused before anything spawns. Plugins reach it under the newinterface
capability. Seedocs/features/interface.md. rowAction: "run"— plugin panels you can operate. A row may name one of
its own plugin's commands plusargsto call it with; clicking invokes it and
re-renders the panel from the answer, so the panel shows what the device
reports rather than what the click assumed.argsride the stdin context
(absent when no row invoked the command, so existing plugins see the context
they always did), and a row can only reach a command in its own plugin. Still
no plugin code in the page — the click is an HTTP call to the same endpoint
the panel opens with. The bundled a11y plugin uses it for a Display & Text
Size picker beside its audit.- Plugin system. Third-party plugins add domain-specific affordances
(an Expo reload button, an accessibility audit, a deep-link bar) without
touching baguette's core. A plugin is a directory with a
baguette-plugin.jsonmanifest declaring toolbar/panel contributions
backed by a command baguette runs as a subprocess — cwd pinned, fresh
environment carryingBAGUETTE_URL/BAGUETTE_UDID/BAGUETTE_TOKEN, a
timeout, and a one-line JSON answer. Nothing a plugin ships is ever loaded
into baguette's process or the served page. Plugins render in a dedicated
plugins rail on the focus-mode screen, deliberately separate from the
device toolbar so an installed plugin can't be mistaken for a core control.
Each plugin takes one rail slot however many tools it ships: a
multi-tool plugin collapses behind a single entry that expands on hover
into a flyout naming each tool. A manifest may declare a top-level
iconfor that collapsed entry, defaulting to its first panel's.
A bundled accessibility-audit plugin ships in the binary as a reference.
CLI:baguette plugin list | show | validate | run. See
docs/features/plugins.md. - Bakeries — plugin distribution. A bakery is any git repo with a
baguette.jsonmenu at its root. Trust a source once
(baguette bakery add owner/repo), then install any plugin it offers
(baguette plugin install <name>, orowner/repo/namedirectly), from the
CLI or the plugins rail's + Add modal. Trust is per bakery — accepting
one means accepting that its plugins run as programs with your permissions —
and installing only copies files; nothing runs until you activate a plugin.
Everything pins to a commit; fetches are shallow, non-interactive, and pull
no submodules. Full lifecycle:bakery add | list | remove | update,
plugin install | remove | update. - Enforced plugin capabilities. A manifest's
capabilitieslist is now a
real permission boundary rather than documentation. Each command invocation
receives its own token carrying exactly the plugin's declared set, revoked
when the command exits; a plugin that didn't declare a capability gets a
403on the matching route even though its token is otherwise valid. Least
privilege by default — declaring nothing grants nothing — and an unknown
capability is a parse error, so typos surface atbaguette plugin validate.
baguette plugin showprints what a plugin may do before you install it.
This replaces the shared session token, which by construction could not tell
one plugin from another. The check runs in front of every route instead of
inside the handful that remembered to ask, so all eight capabilities are
enforced —describe-ui,input,screenshot,logs,status-bar,
location,apps,media,simulators— and routes no capability names (booting a
device, the camera source, installing another plugin) are closed to plugins
by construction. A route added later stays closed until it's mapped. POST /simulators/:udid/input. The gesture pipeline over HTTP, taking
the same envelope the stream socket andbaguette inputaccept, so a plugin
can drive the device without holding a WebSocket open. Gated by theinput
capability. A worked example ships in
examples/expo-bakery/— an installable two-plugin
bakery that sends the React Native ⌘R / ⌘D dev chords.- Plugin contract hardening, ahead of freezing
apiVersion: 1. An
omittedapiVersionnow means 1 permanently rather than "whatever this
build supports", so the day the ceiling moves, manifests written before the
field existed aren't silently reinterpreted. Unknown icons resolve to a
default glyph instead of refusing the manifest — a plugin naming a newer
icon works on an older baguette, and since the author's string is replaced
rather than escaped, untrusted text still never reaches the page.
plugin validatereports the substitution so a typo isn't swallowed. appsandmediareplace thefilescapability. Putting a photo in
the library and putting an executable on the device aren't the same
authority. Splitting them meant splitting the route, because the required
capability is derived from the path alone — that's what makes an unmapped
route closed rather than open.POST /simulators/:udid/fileskeeps
classifying by extension for the browser's drag-and-drop and is now
reachable by no capability at all; plugins use/appsand/media.- The pinned commit is now a demand, not a note. A bakery's recorded sha
used to only describe what a shallow clone happened to fetch, so a source
trusted months ago quietly delivered its current contents. Installs now
fetch the pinned commit by name and verify they landed on it; a remote that
no longer serves it fails rather than falling back to HEAD. baguette bakery outdated. Asks each trusted remote what it points at
now — onels-remoteeach, no clone — and reports which have moved. It only
reports: nothing changes until you runbakery update, since an update that
applied itself would let a source accepted once ship you anything later. An
unreachable remote is reported as unreachable, never as up to date.- Installing a plugin is CLI-only;
POST /bakeries/installis gone.
Preview still runs in the browser — it clones into the cache and reads a
menu. Installing writes files into a directory baguette later executes from,
and the only thing in front of a browser route is a set of origin
heuristics. Theaccept:trueflag wasn't independent consent either: the
modal set the flag the server checked. The rail now previews and hands over
the command to run. - Every spawned child is bounded.
Subprocessgrows akill()
alongsideterminate(). SIGTERM is a request a child may trap or ignore,
and when it does its exit handler never fires — so a plugin command could
holdPluginDispatch.runopen forever, leaving the serve route unanswered
and the per-invocation capability grant live for as long as the child chose
to run. The deadline now escalates to the signal that can't be refused after
a grace period, and reports the outcome as a timeout rather than blaming the
plugin for exiting on a signal the host sent.GitCheckoutgained a deadline
too:GIT_TERMINAL_PROMPT=0only rules out a credential hang, so a remote
that connected and then stalled held aPOST /bakeries/previewtask open
indefinitely. - Shake gesture.
baguette shake --udid <UDID>,POST /simulators/<UDID>/shakeonserve, and a shake button in the serve
UI toolbar (next to Home / App switcher, mirroring the rotate button)
deliver a motion shake to a booted simulator — UIKit fires
motionShakeon the frontmost
responder, the same as Simulator.app's Device → Shake. Backed by
simctl spawn <udid> notifyutil -p com.apple.UIKit.SimulatorShake,
which posts the private UIKit Darwin notification into the guest's
notify namespace (a hostnotify_postnever reaches the iOS guest).
Chosen over the native-[SimDevice gsEventsSendShake]/
PurpleWorkspacePortGSEvent path because the shake body bytes aren't
documented like orientation's — the simctl path is documented,
crash-free, and unit-testable end-to-end. iOS-only by design.
Seedocs/features/shake.md.
What's Changed
- feat(shake): shake a booted simulator via CLI, serve route, and toolbar by @iMattin in #46
- feat: plugin system — manifests, capabilities, and git-backed distribution by @crockalet in #40
New Contributors
Full Changelog: v0.1.89...v0.1.90
baguette v0.1.89
What's Changed
- feat(carplay): dual-pane phone + CarPlay streaming by @Eyadkelleh in #45
New Contributors
- @Eyadkelleh made their first contribution in #45
Full Changelog: v0.1.88...v0.1.89
baguette v0.1.88
Fixed
/devices/device-filter.jsno longer 404s. Thedevices/web-root
subfolder was added without a matching static route (Hummingbird needs
one literal route per subdirectory). The per-subdirectory routes are
now generated from a singleServer.staticAssetSubdirectorieslist,
andStaticAssetRoutesTestspins that list against the folders that
actually exist underResources/Web/— adding a subfolder without
routing it now fails a test instead of 404ing in the browser.
Full Changelog: v0.1.87...v0.1.88
baguette v0.1.87
Fixed
- Homebrew installs no longer crash at startup with
could not load resource bundle.DeviceModelRoots(evaluated on everyserve/
render-3dlaunch) was the last caller of SPM's generated
Bundle.moduleaccessor, which looks inBundle.main.bundleURL— the
symlink's directory (/opt/homebrew/bin) under Homebrew's
bin/baguette → libexec/baguettelayout — andfatalErrors on miss.
It now resolves the sidecarBaguette_Baguette.bundleviadladdr
(which reports the resolved real path), matchingWebRootand
VirtualCameraInstaller; a missing bundle drops the bundled-models
root instead of crashing. - 3D "Interact" mode maps clicks and edge drags correctly at any camera
pose. Previously the whole canvas was treated as a 1:1 crop of the
device screen, so home-indicator/notification-center edge drags either
never triggered (a click at the visual screen edge landed well inside the
canvas) or dispatched at the wrong coordinate once the model was rotated
off Front — the default "Hero" pose.RealityKitDeviceScenenow
analytically projects the screen mesh's four corners for the active pose
(ScreenQuadProjection, pure trig mirroring the renderer's own rotation
and perspective-camera math) and the server pushes them to the browser as
{"type":"screen_quad", ...}after everyset_3d_camera;sim-3d.js
inverse-maps clicks through that quad instead of the raw canvas rect. See
docs/features/3d-rendering.md.
What's Changed
Full Changelog: v0.1.86...v0.1.87