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}