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.
One verb is absent: proxy — Mode-2 deployment plumbing (worker pool,
manifest reload on SIGHUP). It has no dev-loop meaning; mesofact proxy
remains its home. Deliberate, and still true.
check used to be a second one, for a reason that did
not hold (R451, reported by @Ashguard:dragon 2026-09-01): the note read
“running TypeScript needs node or bun on PATH”, and it does not —
typescript@7 ships bin/tsc as a three-line Node shim over a per-platform
native Go binary (node_modules/@typescript/typescript-<platform>-<arch>/lib/tsc)
that mesofact-build’s check.rs resolves and spawns directly.
mes check is implemented as of R832-T4, and the reason it had to be
is sharper than “cheap”: a scaffolded project’s typecheck script has to
name a binary the user can actually reach, and the R759-F2 trampoline
vends exactly three names — mesofact, mes, mesofact-dev. Every other
name, mesofact-build included, exits 70. So the verb had to appear on one
of those three before mesofact new could put it in a package.json.
§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.”)