Expand description
mes — the mesofact dev toolchain.
The whole CLI lives here in the library rather than in a bin target,
mirroring mesofact::cli, so that the two bin targets — mes and the
transitional mesofact-dev alias — are each three lines over one
definition. (They cannot simply share one main.rs: cargo warns when a
file backs two targets, and it compiles the whole CLI twice.)
§Why this is a superset of mesofact, not an alias of it
mes carries the prod verbs (serve, publish) alongside the dev ones
(dev, build), so a consumer learns one CLI. That does not weaken
the W225 §2 dev/prod boundary, because that boundary is about which crates
land in a binary, not about which verbs a binary spells. It only ever
runs one direction: this crate already depends on the mesofact facade, so
mes serve is an in-process call into mesofact::cli — free. The
reverse (making the shipped mesofact binary a shim over mes) would drag
the watcher, the dev S3 surface and rolldown into the prod closure, and is
exactly what §2 forbids. mesofact stays a standalone binary.
Why not spawn the mesofact binary for the prod verbs instead of linking
it? (Asked more than once; the answer is not “in-process is tidier”.) It
would save nothing. mes dev — the 95% command — is built directly on the
facade’s engine: mesofact::Server::from_workload,
mesofact::ssr::spawn, mesofact::SsrSpawnOptions,
mesofact::ProxyMap. The facade is linked into this binary for the DEV
loop whether or not serve is in-process, so process-calling removes zero
bytes and adds a runtime dependency on finding mesofact on PATH —
cargo install mesofact-dev would yield a mes whose serve fails until
you separately install mesofact. It would also hand us exit-code and
signal forwarding for no gain. The link-graph direction is what carries the
security property here; the process boundary carries none of it.
Two verbs are absent:
-
proxy— Mode-2 deployment plumbing (worker pool, manifest reload on SIGHUP). It has no dev-loop meaning;mesofact proxyremains its home. Deliberate, and still true. -
check(tsc --noEmit) — absent for a reason that no longer holds, corrected here rather than left to misinform (R451, reported by @Ashguard:dragon 2026-09-01). This used to read “running TypeScript needs node or bun onPATH”. It does not: typescript@7 shipsbin/tscas a three-line Node shim over a per-platform native Go binary (node_modules/@typescript/typescript-<platform>-<arch>/lib/tsc), andmesofact-build’scheck.rsresolves and spawns that binary directly —PATH=/nonexistent mesofact-build check <dir>runs green and reportserror TS2322with exit 1 on a seeded type error. The illustrated failure the old note gave (“dies withbun: command not found”) was never this path’s failure mode either;check.rshas never invoked bun.So
mes checkis now merely unimplemented, and cheap — this crate already linksmesofact-buildbehind the default-onbuildfeature. Adding it belongs with whichever ticket owns thechecksurface, so that the facade verb and this one land together rather than drifting.
§Bare invocation
mes with no subcommand is mes dev . — the 95% command, so it is what
you get for typing the least. This is also what keeps the legacy
mesofact-dev <DIR> --port N … form working verbatim: it parses as the
bare form, so the ~30 spawn sites in the parent camp need no flag day. The
one divergence is a workload directory literally named after a subcommand
(mesofact-dev serve); spell it ./serve if that ever comes up.
@yah:relay(R822, “Retire the mesofact-dev BINARY; mes is the only dev command (the package name stays)”)
@yah:at(2026-09-02T06:04:50Z)
@yah:status(open)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:next(“OPERATOR DECISION 2026-09-01, and it is the inverse of the split first proposed: descriptive name where you READ it, short name where you TYPE it. The PACKAGE stays mesofact-dev permanently – not as a legacy spelling of mes, but as the meta crate, the entrypoint to the dev-tier tools mes needs that are NOT mesofact (the watcher, the local S3 surface standing in for R2, serve_app). It depends on the mesofact facade rather than being it, and that direction IS the W225 section 2 prod/dev boundary. The BINARY mesofact-dev goes: nobody wants to type it, ever.”)
@yah:next(“SCOPE, measured not guessed: 30 Rust string-literal sites resolve "mesofact-dev" as an executable name (grep ‘"mesofact-dev"’ app crates –include=*.rs, excluding @yah: annotation prose, which is the other ~170 hits and is history rather than reference). The load-bearing ones: crates/yah/plugin/src/source.rs:112 keys a builtin plugin manifest on it via include_str!; crates/yah/bundled/src/lib.rs:171 registers it as a bundled binary; app/yah/desktop/src/mesofact_versions.rs:80,94 resolve it through yah_bundled::find and slot.bin(); app/yah/cli/src/plugin_host.rs:414 names it as SourceRef::Bundled.”)
@yah:next(“THE SHARP EDGE, and the reason this is a migration rather than a find-and-replace: ALREADY-INSTALLED store slots have a file literally named mesofact-dev on disk. app/yah/desktop/src/mesofact_versions.rs does slot.bin("mesofact-dev"), so renaming the bin breaks resolution against versions a user already installed. Needs a compat window that accepts either filename, or a store migration – decide which before touching the 30 sites.”)
@yah:next(“ALSO IN SCOPE, all naming the bin rather than the package: .yah/qed/release-build.toml builds package=mesofact-dev bin=mesofact-dev (-> bin=mes); scripts/check-mesofact-store.sh asserts a slot holds mesofact AND mesofact-dev and that shims exist for mesofact/mes/mesofact-dev; scripts/check-install-sh-compat.sh compares the mesofact-dev shim; oss/mesofact/scripts/check-mesofact-new.sh copies target/debug/mesofact-dev into the test slot and runs mesofact-dev ..”)
@yah:next(“AND THE LEGACY-FORM LOGIC ITSELF: cli.rs’s bare-invocation design exists specifically so mesofact-dev <DIR> --port N parses as mes dev <DIR>, which is what let the parent camp’s spawn sites keep working without a flag day. Once the bin is gone that rationale is spent – the bare form is still right for mes, but the module doc’s justification for it needs rewriting rather than deleting.”)
@yah:next(“NOT BLOCKING THE CRATES.IO PUBLISH. Because the package name is unchanged, mesofact-dev@0.8.29 can be published before any of this lands; removing a bin target later is an ordinary deprecation (cargo install at 0.8.29 gets both binaries, at a later version gets only mes). This ticket was originally framed as racing that publish – it is not.”)
@yah:next(“END STATE DECIDED BY OPERATOR 2026-09-01, and it supersedes the narrower ‘drop the second [[bin]]’ framing this ticket opened with: mesofact-dev has NO BIN TARGETS AT ALL. It becomes a pure library – the meta crate, the entrypoint to the dev-tier tools that are not mesofact. A new thin package mes owns the binary: one src/main.rs, three lines over mesofact_dev::cli::run(), depending on mesofact-dev.”)
@yah:next(“WHY THIS SHAPE RATHER THAN JUST DELETING THE mesofact-dev BIN: cargo install takes a PACKAGE name, not a bin name. With the binary living in the mesofact-dev package, cargo install mes fails with ‘could not find mes in registry’ and users must know to type cargo install mesofact-dev – unguessable from the command. Splitting gives library named for what it IS and binary named for what you TYPE, and removes the bin-collision footgun of two packages both emitting ~/.cargo/bin/mes.”)
@yah:next(“SAFE TO DO: verified 2026-09-01 that NO crate anywhere takes mesofact-dev as a library dependency today – the only line in the tree is the library-tier scaffold template (crates/mesofact/src/cli/new/template-lib/Cargo.toml:31), which wants the library and is unaffected. So nothing loses a binary it was consuming, and the new mes package is the first real lib consumer.”)
@yah:gotcha(“SUPERSEDED NOTE, removed rather than left to mislead: an earlier gotcha here said the end state was ‘package mesofact-dev while the only [[bin]] is mes’. It is not. The end state is mesofact-dev with ZERO bin targets and a separate mes package owning the binary. The cargo package-vs-bin decoupling still matters, but it is now the reason the LIBRARY can keep a descriptive name while the COMMAND gets a short one across a package boundary, not within one.”)
@yah:gotcha(“FEATURE FORWARDING IS THE FIDDLY PART, and this crate already documents the same trap one level down. mesofact-dev’s surface is default = [ssr, build]; ssr = [mesofact/ssr, dep:mesofact-publisher, publish]; publish = [mesofact/publish]; build = [mesofact/build]. The new mes package must re-expose these (ssr = [mesofact-dev/ssr], etc.) or cargo install mes --no-default-features silently loses the lean static/SPA path that exists so consumers can skip the V8 toolchain. mes itself has no cfgs – cli.rs’s #[cfg(feature = …)] gates evaluate in mesofact-dev – so forwarding is all that is required, but omitting it is invisible until someone tries the lean build.”)