Skip to main content

aube_util/
identity.rs

1//! Compile-time embedder profile — the binary's identity and embedder-fixed
2//! behavior, centralized.
3//!
4//! aube hardcodes its own name, version, lockfile filename, cache namespace,
5//! env-var prefix, and so on across many crates, and bakes in a handful of
6//! behavior choices that an embedding host would want to flip. [`Embedder`]
7//! gathers those *embedder-fixed* values — branding plus the behavior toggles
8//! that are the host's to set, not the user's — into one place, so the
9//! binary's identity is selected once, at the entry point, instead of being
10//! scattered as literals and policy checks. Standalone aube ships [`AUBE`],
11//! which reproduces every value verbatim, and consumers read it through
12//! [`embedder`].
13//!
14//! This struct holds *embedder-fixed* data: branding (pure naming constants)
15//! plus the four behavior toggles that an embedder — not an end user — owns
16//! (`canonical_lockfile_always_wins`, `runtime_switching`, `self_engines_check`,
17//! `self_update_enabled`). Genuinely *user-tunable* knobs do not belong here;
18//! those stay settings.
19//!
20//! An embedder selects its profile by registering it with [`set_embedder`].
21//! A host that goes through the library entry point `aube::cli_main` passes
22//! its `&'static Embedder` there and `cli_main` registers it; a host that
23//! drives the command layer in-process (`aube::commands::*::run`, bypassing
24//! `cli_main` — the headline embedding use case) calls [`set_embedder`] itself
25//! at startup. Internally the chosen profile is stored once in a private
26//! [`OnceLock`]; [`embedder`] returns it, falling back to [`AUBE`] when nothing
27//! was registered, so any caller or test that never sets one transparently
28//! gets standalone-aube behavior.
29
30use std::sync::OnceLock;
31
32/// The binary's embedder profile — branding plus embedder-fixed behavior.
33///
34/// Branding fields are pure naming constants. The behavior toggles
35/// (`canonical_lockfile_always_wins`, `runtime_switching`,
36/// `self_engines_check`, `self_update_enabled`) are embedder-fixed, not
37/// user-tunable: a host that mirrors the project's incumbent package manager,
38/// owns Node provisioning, lives outside aube's version namespace, or owns its
39/// own self-update flips them. Genuinely user-tunable knobs stay settings.
40#[derive(Clone, Copy, Debug)]
41pub struct Embedder {
42    /// Tool name, lowercase (e.g. `"aube"`). The proper noun users type and
43    /// see in output, and the clap command name driving help/usage/errors.
44    /// Must be filesystem- and command-safe (no spaces, slashes, or shell
45    /// metacharacters); it is used verbatim in on-disk sidecar paths (e.g.
46    /// `.<name>_patch_state.json`, `.<name>-deploy-injected/`) and in command
47    /// invocations, so the embedder is responsible for supplying a safe slug.
48    pub name: &'static str,
49    /// High-visibility display name shown in the progress banner (e.g.
50    /// `"aube"`). Usually equal to [`name`](Self::name); split out so an
51    /// embedder can brand the banner independently of the command name.
52    pub display_name: &'static str,
53    /// Vendor attribution rendered after the version in the progress banner,
54    /// e.g. `Some("by jdx.dev")`. `None` suppresses the attribution entirely
55    /// (an embedder that doesn't want a third-party vendor tag).
56    pub vendor: Option<&'static str>,
57    /// Version string — `env!("CARGO_PKG_VERSION")` for standalone aube.
58    pub version: &'static str,
59    /// HTTP `User-Agent` product token, e.g. `"aube/1.19.0"`. Sent to the
60    /// registry and exported as the lifecycle `npm_config_user_agent`
61    /// product.
62    pub user_agent: &'static str,
63    /// Names this tool recognizes as *itself* in a `packageManager` field or
64    /// a lockfile-kind detection. Standalone aube: `["aube"]`.
65    pub self_names: &'static [&'static str],
66    /// Names accepted as compatible drop-in targets in the `packageManager`
67    /// guardrail. Standalone aube: `["pnpm"]`.
68    pub compatible_names: &'static [&'static str],
69    /// Canonical lockfile filename, e.g. `"aube-lock.yaml"`.
70    ///
71    /// Invariant (checked in [`set_embedder`]): must contain a `.` (so the
72    /// stem/extension split the lockfile-candidate machinery relies on holds)
73    /// and must not collide with a foreign package manager's lockfile name
74    /// (`pnpm-lock.yaml`, `package-lock.json`, `bun.lock`, `yarn.lock`,
75    /// `npm-shrinkwrap.json`). Aliasing a foreign name would make aube's own
76    /// lockfile indistinguishable from the incumbent's in the
77    /// lockfile-candidate set (`io.rs` / `clean.rs` / `pack.rs`).
78    pub lockfile_basename: &'static str,
79    /// The *branded* workspace-config YAML this tool reads and writes, e.g.
80    /// `"aube-workspace.yaml"`. `None` disables the tool's own branded YAML
81    /// entirely (the shared `pnpm-workspace.yaml` compatibility surface is
82    /// handled separately and is not configured here).
83    pub workspace_yaml: Option<&'static str>,
84    /// The `package.json` object key this tool reads its own config under,
85    /// e.g. `"aube"`. `""` means this tool has *no own* branded manifest
86    /// namespace: config reads fold only the
87    /// [`compatible_names`](Self::compatible_names) namespaces plus any
88    /// top-level (manifest-root) entry, and setting *writes* go to the
89    /// manifest **root** as top-level `package.json` keys — never under a
90    /// foreign brand's namespace, and never as a literal `""` key.
91    pub manifest_namespace: &'static str,
92    /// Env-var prefix for the tool's *internal* debug / diagnostic / perf-bisect
93    /// toggles, read through [`embedder_env`](crate::env::embedder_env), e.g.
94    /// `Some("AUBE")` → `AUBE_DISABLE_CLONEDIR`, `AUBE_DIAG_PRINT`, … `None`
95    /// means the tool exposes *no* branded debug-toggle family — every such
96    /// toggle is simply unreadable, so an embedding host's brand never sprouts a
97    /// dozen `<HOST>_DISABLE_*` perf switches. This gates the non-settings,
98    /// non-user-facing toggle family only; the few user-facing config knobs go
99    /// through [`config_env_prefix`](Self::config_env_prefix), and the settings
100    /// table's branded aliases go through
101    /// [`branded_env_alias_enabled`](crate::env::branded_env_alias_enabled).
102    pub env_prefix: Option<&'static str>,
103    /// Env-var prefix for the tool's small set of *first-class config* knobs —
104    /// the cache dir and the fetch concurrency — read through
105    /// [`config_env`](crate::env::config_env), e.g. `Some("AUBE")` →
106    /// `AUBE_CACHE_DIR` / `AUBE_CONCURRENCY`, `Some("NUB")` → `NUB_CACHE_DIR` /
107    /// `NUB_CONCURRENCY`. Distinct from [`env_prefix`](Self::env_prefix): these
108    /// few knobs ARE legitimate config the host wants under its own brand,
109    /// whereas the debug toggles vanish under an embedder that hides them.
110    /// `None` reads no first-class config env.
111    pub config_env_prefix: Option<&'static str>,
112    /// Leaf directory name under the OS cache root, e.g. `"aube"` →
113    /// `<XDG_CACHE_HOME>/aube`.
114    pub cache_namespace: &'static str,
115    /// Leaf directory name under the OS data/state root, e.g. `"aube"`.
116    pub data_namespace: &'static str,
117
118    // --- embedder-fixed behavior toggles (not user-tunable) ---
119    /// When `true` (aube's default), this tool's canonical lockfile
120    /// (`lockfile_basename`) outranks any foreign lockfile present in
121    /// lockfile-kind detection. An embedder that mirrors the project's
122    /// incumbent package manager sets this `false` so the incumbent's
123    /// lockfile wins instead. Embedder-fixed: it's the host's call, not the
124    /// user's.
125    pub canonical_lockfile_always_wins: bool,
126    /// When `true` (aube's default), this tool resolves and switches the Node
127    /// runtime from version files / devEngines and prepends it to `PATH`. An
128    /// embedder that owns Node provisioning itself sets this `false`, leaving
129    /// the runtime resolver inert. Embedder-fixed.
130    pub runtime_switching: bool,
131    /// When `true` (aube's default), this tool validates a manifest's
132    /// `engines.<self>` constraint against its own version. An embedder whose
133    /// version isn't in aube's version namespace sets this `false` to avoid
134    /// spurious `engines.aube` mismatches. The `engines.node` check is
135    /// unaffected. Embedder-fixed.
136    pub self_engines_check: bool,
137    /// When `true` (aube's default), this tool owns its own self-update:
138    /// the update notifier (and its `aube.jdx.dev` endpoints) runs. An
139    /// embedder that owns its own upgrade path sets this `false` so those
140    /// code paths never run. Embedder-fixed.
141    pub self_update_enabled: bool,
142}
143
144/// Standalone aube's embedder profile. Reproduces every hardcoded branding
145/// constant and behavior default verbatim; this is the fallback whenever no
146/// profile is registered.
147pub const AUBE: Embedder = Embedder {
148    name: "aube",
149    display_name: "aube",
150    vendor: Some("by jdx.dev"),
151    version: env!("CARGO_PKG_VERSION"),
152    user_agent: concat!("aube/", env!("CARGO_PKG_VERSION")),
153    self_names: &["aube"],
154    compatible_names: &["pnpm"],
155    lockfile_basename: "aube-lock.yaml",
156    workspace_yaml: Some("aube-workspace.yaml"),
157    manifest_namespace: "aube",
158    env_prefix: Some("AUBE"),
159    config_env_prefix: Some("AUBE"),
160    cache_namespace: "aube",
161    data_namespace: "aube",
162    canonical_lockfile_always_wins: true,
163    runtime_switching: true,
164    self_engines_check: true,
165    self_update_enabled: true,
166};
167
168static ACTIVE: OnceLock<&'static Embedder> = OnceLock::new();
169
170/// Register the active embedder profile.
171///
172/// Call this **once at startup**, before invoking any `aube::commands`
173/// directly. `aube::cli_main` calls it for you, so binaries that go through
174/// `cli_main` don't need to; embedders that drive the command layer in-process
175/// — calling `aube::commands::*::run` directly, bypassing `cli_main` (the
176/// headline embedding use case) — call it themselves to register their
177/// profile before the first command runs.
178///
179/// Set-once / first-wins: the first registration is the active profile for the
180/// process; later calls are silently ignored. A process that never registers
181/// one transparently gets standalone-aube behavior ([`AUBE`]) — which is also
182/// why tests that don't register a profile see `AUBE`.
183///
184/// Validates the profile's lockfile invariant in debug builds: a profile whose
185/// `lockfile_basename` has no extension or aliases a foreign package manager's
186/// lockfile would silently corrupt the lockfile-candidate set, so it trips a
187/// `debug_assert!` here — at registration, the single choke point — rather than
188/// misbehaving deep inside `io.rs` / `clean.rs` / `pack.rs`.
189pub fn set_embedder(embedder: &'static Embedder) {
190    debug_assert!(
191        embedder.lockfile_basename.contains('.'),
192        "embedder lockfile_basename {:?} must contain a `.` (stem/extension split is load-bearing)",
193        embedder.lockfile_basename,
194    );
195    debug_assert!(
196        !FOREIGN_LOCKFILE_NAMES.contains(&embedder.lockfile_basename),
197        "embedder lockfile_basename {:?} aliases a foreign package manager's lockfile; \
198         pick a distinct name so aube's lockfile stays distinguishable in the candidate set",
199        embedder.lockfile_basename,
200    );
201    let _ = ACTIVE.set(embedder);
202}
203
204/// Foreign package-manager lockfile names an embedder's `lockfile_basename`
205/// must not alias. Aliasing one would make aube's own lockfile collide with
206/// the incumbent's in the lockfile-candidate machinery.
207const FOREIGN_LOCKFILE_NAMES: &[&str] = &[
208    "pnpm-lock.yaml",
209    "package-lock.json",
210    "bun.lock",
211    "yarn.lock",
212    "npm-shrinkwrap.json",
213];
214
215/// The active embedder profile, or [`AUBE`] when none was registered. Never
216/// panics: an unset profile transparently yields standalone-aube behavior.
217pub fn embedder() -> &'static Embedder {
218    ACTIVE.get().copied().unwrap_or(&AUBE)
219}
220
221/// The active tool's program name for *user-facing* output — the proper noun a
222/// user types and reads (e.g. `"aube"` under the default profile, the host's
223/// brand under an embedder).
224///
225/// This is the source-branding seam jdx approved over post-processing rendered
226/// output: instead of an embedder string-rewriting `"aube"` out of finished
227/// banners and error text, user-facing emission sites compose the program name
228/// at the source via `prog()` / [`cmd`], so a library consumer (nub) gets its
229/// own brand without any post-pass. Use it for bare program-name references in
230/// `miette!` / `bail!` / `eprintln!` / `println!` strings (banners, "re-exec
231/// into pinned {prog} version", …). For an `aube <verb>` command reference use
232/// [`cmd`] instead, which also brands the verb's program prefix.
233///
234/// Default-preserving: under the default [`AUBE`] profile this returns exactly
235/// `"aube"` byte-for-byte, so standalone aube's output is unchanged. Returns
236/// [`Embedder::name`] — the command-safe slug, matching what the clap command
237/// name and on-disk sidecars already use, so a user reads one consistent name.
238pub fn prog() -> &'static str {
239    embedder().name
240}
241
242/// A *user-facing* `"{prog} <verb>"` command reference, e.g. `cmd("install")`
243/// renders `"aube install"` under the default profile and `"nub install"` under
244/// a `nub`-branded embedder.
245///
246/// Use this wherever a user-facing `miette!` / `bail!` / `eprintln!` /
247/// `println!` string tells the user to run a command — `"run `{}` first"` with
248/// `cmd("install")`, `"`{}`: package has no script"` with `cmd("run")`, help
249/// hints, and so on — so the program prefix follows the active brand instead of
250/// being hardcoded to `aube`. The `verb` is the command spelling exactly as the
251/// CLI accepts it (`"install"`, `"patch-commit"`, `"store prune"`); it is not
252/// re-branded, only the leading program name is.
253///
254/// Default-preserving: under the default [`AUBE`] profile `cmd("install")` is
255/// exactly `"aube install"` byte-for-byte, so standalone aube's error and help
256/// text is unchanged. Allocates a `String`; for the bare program name with no
257/// verb use [`prog`], which borrows.
258pub fn cmd(verb: &str) -> String {
259    format!("{} {verb}", prog())
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    /// With no profile registered, `embedder()` is `AUBE` and every field
267    /// reproduces aube's standalone branding and behavior defaults verbatim.
268    /// This is the behavior-neutrality contract: an embedder that sets nothing
269    /// gets aube.
270    ///
271    /// Relies on no other test in this binary calling `set_embedder` — the
272    /// `ACTIVE` `OnceLock` is process-global and first-write-wins, so a test
273    /// that registers a non-aube profile would flip the fallback this asserts.
274    /// Keep profile registration out of this crate's unit tests.
275    #[test]
276    fn embedder_unset_is_aube() {
277        let id = embedder();
278        assert_eq!(id.name, "aube");
279        assert_eq!(id.display_name, "aube");
280        assert_eq!(id.vendor, Some("by jdx.dev"));
281        assert_eq!(id.version, env!("CARGO_PKG_VERSION"));
282        assert_eq!(id.user_agent, concat!("aube/", env!("CARGO_PKG_VERSION")));
283        assert_eq!(id.self_names, &["aube"]);
284        assert_eq!(id.compatible_names, &["pnpm"]);
285        assert_eq!(id.lockfile_basename, "aube-lock.yaml");
286        assert_eq!(id.workspace_yaml, Some("aube-workspace.yaml"));
287        assert_eq!(id.manifest_namespace, "aube");
288        assert_eq!(id.env_prefix, Some("AUBE"));
289        assert_eq!(id.config_env_prefix, Some("AUBE"));
290        assert_eq!(id.cache_namespace, "aube");
291        assert_eq!(id.data_namespace, "aube");
292        assert!(id.canonical_lockfile_always_wins);
293        assert!(id.runtime_switching);
294        assert!(id.self_engines_check);
295        assert!(id.self_update_enabled);
296    }
297
298    /// Under the default (AUBE) profile the source-branding helpers reproduce
299    /// aube's hardcoded user-facing strings byte-for-byte: `prog()` is `"aube"`
300    /// and `cmd("install")` is `"aube install"`. This is the default-preserving
301    /// contract for the helpers jdx approved — converting a literal `"aube
302    /// install"` site to `cmd("install")` changes nothing for standalone aube.
303    /// (The non-aube branch — a host brand flowing through `prog`/`cmd` — is
304    /// covered by the `source_branding_brand_gate` integration test, which
305    /// registers a real profile in its own process; doing it here would flip
306    /// the process-global fallback the default-profile tests depend on.)
307    #[test]
308    fn prog_and_cmd_render_aube_under_default_profile() {
309        assert_eq!(prog(), "aube");
310        assert_eq!(cmd("install"), "aube install");
311        assert_eq!(cmd("patch-commit"), "aube patch-commit");
312        assert_eq!(cmd("store prune"), "aube store prune");
313    }
314}