Skip to content

Releases: tddworks/baguette

baguette v0.1.96

Choose a tag to compare

@github-actions github-actions released this 24 Aug 09:40
957610d

Fixed

  • serve ran away to 9+ GB within minutes of streaming. Reading
    framebufferSurface is not a local property read: it forwards through
    ROCKit to CoreSimulatorService as a synchronous XPC round-trip. Every
    framebuffer notification scheduled its own queue.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 live xpc_uuid_t and 17k IOSurface. PendingCapture now coalesces
    notifications onto the one already-queued capture — every capture reads the
    latest surface, so a duplicate would only refetch the same frame — and
    captureLatest drains its own autorelease pool. Measured over 6 minutes of
    streaming, walking and 12 stream restarts: peak 143 MB, versus 1489 MB
    before. Note ps rss cannot 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 new Task onto 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. FrameBacklog bounds 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
    carplaySession without 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, but AVCCStream re-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

Choose a tag to compare

@github-actions github-actions released this 21 Aug 06:55

Full Changelog: v0.1.94...v0.1.95

baguette v0.1.94

Choose a tag to compare

@github-actions github-actions released this 20 Aug 09:11
7539036

Fixed

  • A new injected dylib silently broke the Homebrew build. Each one needed
    its own stanza in build.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 both brew audit ("Unexpected universal binaries were found") and
    relocation ("Updated load commands do not fit in the header").
    Injected/build.sh now loops over Injected/*/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. clang exits 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 unrelated brew audit failure 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_ARCHS narrows that to a single host slice for
    packagers, and every slice is now linked with
    -headerpad_max_install_names so Homebrew can rewrite the install ID
    during relocation.

Removed

  • Stale top-level VirtualCamera/VirtualCamera.dylib, left behind when the
    injected dylibs moved under Injected/.

What's Changed

  • fix(build): build injected dylibs from one loop, not four hardcoded lists by @hanrw in #67

Full Changelog: v0.1.93...v0.1.94

baguette v0.1.93

Choose a tag to compare

@github-actions github-actions released this 20 Aug 03:04

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
    3g means what everyone already means by 3G. NLC itself, and the
    dnctl / pfctl rules 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 set are 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 only NSURLConnection
    and NSURLSession.shared, and against a real React Native app it caught
    zero of the app's own requests over 100 seconds. Swizzling
    +defaultSessionConfiguration is what reaches fetch, 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
    plain baguette network reports 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. URLSessionWebSocketTask gets its
    own hooks and takes latency, loss and offline (not bandwidth — an app
    cannot observe a partial message). Not conditioned: WKWebView page
    loads, NWConnection/Network.framework, raw sockets, and realtime SDKs
    that open their own socket — Ably's ably-cocoa vendors SocketRocket, so
    --offline will 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/install names a bakery by its recorded
    id
    , never a URL or a git ref — so a request can only reach a source
    already in bakeries.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 add stays 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 is InstallDecision in Domain/Bakery/ with
    every refusal path unit-tested, and installing still only copies
    files — nothing runs until you open the plugin's panel.
    GET /bakeries.json now 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.clone emptied 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 terminal bakery 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-rails alongside the plugin rail, and .right-rails is
    position: fixed — which creates a stacking context whatever its
    z-index says. So the modal's z-index: 60 was weighed against its
    siblings inside the rail rather than against the page, and the device's
    z-index: 2 screen 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-rails claimed the opposite ("with z-index: auto
    this container doesn't create a stacking context") and has been corrected
    position: fixed alone 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

Choose a tag to compare

@github-actions github-actions released this 18 Aug 09:23
a8f64f9

Added

  • Motion — baguette motion start|set|stop, and a walk that drives it.
    Makes a simulator's apps read CMMotionActivity (walking, running,
    cycling, automotive), CMPedometer counters, and CMMotionManager
    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 start see 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
    drops course to -1. Motion stays opt-in — moving the device never
    arms a simulator-wide DYLD_INSERT_LIBRARIES on 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 stored w,x,y,z against a
    public x,y,z,w, its gravity is derived from attitude rather than set,
    it ignores its own timestamp: argument, and CMMotionActivity built 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), plus 1920x1080 and 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: square on 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.

    See docs/features/capture-size.md.

  • baguette record — video straight from the CLI. A booted simulator,
    an --output, and either --duration or Ctrl-C; the file is flushed
    and playable either way. Takes --size / --fit / --fps /
    --bitrate, writes .mp4 or .mov (the extension picks the
    container).

    docs/features/recording.md has 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.png can
    never quietly hold JPEG bytes), plus GET …/screenshot.png and
    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.jpg with 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, so appstore-6.9 really is 1290 × 2796 instead of an upscale of
    a video still. render-3d --size and 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> and baguette 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 and exp+ aliases). Unlike
    simctl openurl / idb open / Maestro, openurl warns that https:// 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 listapps gives the roster and each app's Path but not
    CFBundleURLTypes, so those come from <Path>/Info.plist. Over HTTP as
    POST /simulators/:udid/openurl and GET /simulators/:udid/schemes.json,
    reachable by a plugin holding the new open-url capability — which is apart
    from apps, since this only launches software that is already installed.
    See docs/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 a11y still 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 own source command with args, which is the path rowAction: "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 via rowAction: "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 — apiVersion stays 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.js injected into document.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 white var(--panel) text field inside a dark glass modal, and an
    accent pulled from sim.html's light-only --accent rather 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, a 600 10px/0.06em accent
    heading against the app's 700 9.5px/0.10em faint one, a fourth primary
    button, and four 2px accent seams no other surface has.

    All of it now lives in sim-native.html under #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 (see docs/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-warn and --nv-scrim theme 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 #b91c1c on a near-black page. Plugin row
    severity is now a data-severity attribute the stylesheet colours, rather
    than an inline hex the module carries.

What's Changed

Read more

baguette v0.1.91

Choose a tag to compare

@github-actions github-actions released this 14 Aug 03:21
638018c

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
    from IndigoHIDTargetForScreen, 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=carplay asks 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 same Display.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.
    IndigoHIDMessageForMouseNSEvent reads NSEvent thread-local state, so it must
    be built on the main actor. The POST …/input route 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 to 46vw at 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

Choose a tag to compare

@github-actions github-actions released this 11 Aug 14:33
f4c1416

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, mirroring simctl ui itself. Over
    HTTP as GET /simulators/:udid/interface.json and POST /simulators/:udid/interface (any subset; answers with the resulting state).
    The gotcha worth preserving: a read can answer unknown (device not booted)
    or unsupported, 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 new interface
    capability. See docs/features/interface.md.
  • rowAction: "run" — plugin panels you can operate. A row may name one of
    its own plugin's commands plus args to 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. args ride 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.json manifest declaring toolbar/panel contributions
    backed by a command baguette runs as a subprocess — cwd pinned, fresh
    environment carrying BAGUETTE_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
    icon for 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.json menu at its root. Trust a source once
    (baguette bakery add owner/repo), then install any plugin it offers
    (baguette plugin install <name>, or owner/repo/name directly), 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 capabilities list 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
    403 on 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 at baguette plugin validate.
    baguette plugin show prints 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 and baguette input accept, so a plugin
    can drive the device without holding a WebSocket open. Gated by the input
    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
    omitted apiVersion now 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 validate reports the substitution so a typo isn't swallowed.
  • apps and media replace the files capability. 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/files keeps
    classifying by extension for the browser's drag-and-drop and is now
    reachable by no capability at all; plugins use /apps and /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 — one ls-remote each, no clone — and reports which have moved. It only
    reports: nothing changes until you run bakery 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/install is 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. The accept:true flag 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. Subprocess grows a kill()
    alongside terminate(). SIGTERM is a request a child may trap or ignore,
    and when it does its exit handler never fires — so a plugin command could
    hold PluginDispatch.run open 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. GitCheckout gained a deadline
    too: GIT_TERMINAL_PROMPT=0 only rules out a credential hang, so a remote
    that connected and then stalled held a POST /bakeries/preview task open
    indefinitely.
  • Shake gesture. baguette shake --udid <UDID>, POST /simulators/<UDID>/shake on serve, 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
    motionShake on 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 host notify_post never reaches the iOS guest).
    Chosen over the native -[SimDevice gsEventsSendShake] /
    PurpleWorkspacePort GSEvent 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.
    See docs/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

Choose a tag to compare

@github-actions github-actions released this 09 Aug 10:50
c81f2ee

What's Changed

  • feat(carplay): dual-pane phone + CarPlay streaming by @Eyadkelleh in #45

New Contributors

Full Changelog: v0.1.88...v0.1.89

baguette v0.1.88

Choose a tag to compare

@github-actions github-actions released this 01 Aug 17:32

Fixed

  • /devices/device-filter.js no longer 404s. The devices/ 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 single Server.staticAssetSubdirectories list,
    and StaticAssetRoutesTests pins that list against the folders that
    actually exist under Resources/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

Choose a tag to compare

@github-actions github-actions released this 01 Aug 17:01

Fixed

  • Homebrew installs no longer crash at startup with could not load resource bundle. DeviceModelRoots (evaluated on every serve /
    render-3d launch) was the last caller of SPM's generated
    Bundle.module accessor, which looks in Bundle.main.bundleURL — the
    symlink's directory (/opt/homebrew/bin) under Homebrew's
    bin/baguette → libexec/baguette layout — and fatalErrors on miss.
    It now resolves the sidecar Baguette_Baguette.bundle via dladdr
    (which reports the resolved real path), matching WebRoot and
    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. RealityKitDeviceScene now
    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 every set_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

  • Refactor device and gesture filtering with comprehensive tests by @hanrw in #44

Full Changelog: v0.1.86...v0.1.87