diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-core/src/composer.rs | 260 | ||||
| -rw-r--r-- | crates/mozart-core/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/archive.rs | 36 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config.rs | 131 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config_helpers.rs | 59 | ||||
| -rw-r--r-- | crates/mozart/src/commands/dump_autoload.rs | 17 | ||||
| -rw-r--r-- | crates/mozart/src/commands/exec.rs | 42 | ||||
| -rw-r--r-- | crates/mozart/src/commands/run_script.rs | 48 |
8 files changed, 331 insertions, 263 deletions
diff --git a/crates/mozart-core/src/composer.rs b/crates/mozart-core/src/composer.rs new file mode 100644 index 0000000..dd7c9a6 --- /dev/null +++ b/crates/mozart-core/src/composer.rs @@ -0,0 +1,260 @@ +//! Composer-equivalent root state: composer.json + effective config. +//! +//! Mirrors the role of `Composer\Composer` (PHP) to the extent that command +//! handlers need today: a single struct loaded from the project directory, +//! exposing a `config()` accessor over the merged Composer config. +//! +//! See `Composer\Command\BaseCommand::requireComposer()` / +//! `Composer\Command\BaseCommand::tryComposer()` for the upstream contract +//! that [`Composer::require`] and [`Composer::try_load`] are modelled on. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +// ─── composer_home ──────────────────────────────────────────────────────────── + +/// Return the Composer home directory, respecting `COMPOSER_HOME` and falling +/// back to the platform default using Composer-compatible logic. +/// +/// On Unix: +/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists), +/// prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`). +/// - Always include `$HOME/.composer` as a fallback candidate. +/// - Return the first candidate directory that exists on disk; +/// if none exist, return the first candidate. +pub fn composer_home() -> PathBuf { + if let Ok(val) = std::env::var("COMPOSER_HOME") + && !val.is_empty() + { + return PathBuf::from(val); + } + + #[cfg(target_os = "windows")] + { + if let Ok(appdata) = std::env::var("APPDATA") + && !appdata.is_empty() + { + return PathBuf::from(appdata).join("Composer"); + } + return PathBuf::from("C:/ProgramData/ComposerSetup/bin"); + } + + #[cfg(not(target_os = "windows"))] + { + let home_dir = std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")); + + let mut candidates: Vec<PathBuf> = Vec::new(); + + if use_xdg() { + let xdg_config = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| home_dir.join(".config")); + candidates.push(xdg_config.join("composer")); + } + + candidates.push(home_dir.join(".composer")); + + // Return first candidate that exists; otherwise return the first + candidates + .iter() + .find(|p| p.is_dir()) + .cloned() + .unwrap_or_else(|| candidates.into_iter().next().unwrap()) + } +} + +#[cfg(not(target_os = "windows"))] +fn use_xdg() -> bool { + std::env::vars().any(|(k, _)| k.starts_with("XDG_")) + || std::path::Path::new("/etc/xdg").is_dir() +} + +// ─── ComposerConfig ─────────────────────────────────────────────────────────── + +/// Effective Composer config key/value pairs for a project. +/// Keys mirror `Composer\Config`'s defaults; values are stored as raw +/// `serde_json::Value` so callers can re-interpret them per key. +pub struct ComposerConfig { + pub values: BTreeMap<String, serde_json::Value>, +} + +impl ComposerConfig { + /// Build a `ComposerConfig` populated with Composer's built-in defaults. + pub fn defaults() -> Self { + let mut m: BTreeMap<String, serde_json::Value> = BTreeMap::new(); + + m.insert("process-timeout".to_string(), serde_json::json!(300)); + m.insert("use-include-path".to_string(), serde_json::json!(false)); + m.insert("preferred-install".to_string(), serde_json::json!("dist")); + m.insert("notify-on-install".to_string(), serde_json::json!(true)); + m.insert( + "github-protocols".to_string(), + serde_json::json!(["https", "ssh", "git"]), + ); + m.insert("vendor-dir".to_string(), serde_json::json!("vendor")); + m.insert( + "bin-dir".to_string(), + serde_json::json!("{$vendor-dir}/bin"), + ); + m.insert("bin-compat".to_string(), serde_json::json!("auto")); + m.insert("cache-dir".to_string(), serde_json::json!("{$home}/cache")); + m.insert( + "cache-files-dir".to_string(), + serde_json::json!("{$cache-dir}/files"), + ); + m.insert( + "cache-repo-dir".to_string(), + serde_json::json!("{$cache-dir}/repo"), + ); + m.insert( + "cache-vcs-dir".to_string(), + serde_json::json!("{$cache-dir}/vcs"), + ); + m.insert("cache-files-ttl".to_string(), serde_json::json!(15_552_000)); + m.insert( + "cache-files-maxsize".to_string(), + serde_json::json!("300MiB"), + ); + m.insert("cache-read-only".to_string(), serde_json::json!(false)); + m.insert("prepend-autoloader".to_string(), serde_json::json!(true)); + m.insert("autoloader-suffix".to_string(), serde_json::Value::Null); + m.insert("optimize-autoloader".to_string(), serde_json::json!(false)); + m.insert("sort-packages".to_string(), serde_json::json!(false)); + m.insert( + "classmap-authoritative".to_string(), + serde_json::json!(false), + ); + m.insert("apcu-autoloader".to_string(), serde_json::json!(false)); + m.insert("platform".to_string(), serde_json::json!({})); + m.insert("platform-check".to_string(), serde_json::json!("php-only")); + m.insert("lock".to_string(), serde_json::json!(true)); + m.insert("discard-changes".to_string(), serde_json::json!(false)); + m.insert("archive-format".to_string(), serde_json::json!("tar")); + m.insert("archive-dir".to_string(), serde_json::json!(".")); + m.insert("htaccess-protect".to_string(), serde_json::json!(true)); + m.insert("secure-http".to_string(), serde_json::json!(true)); + m.insert("allow-plugins".to_string(), serde_json::json!({})); + + Self { values: m } + } + + /// Merge `overrides` on top of the current values. + pub fn merge(&mut self, overrides: &BTreeMap<String, serde_json::Value>) { + for (k, v) in overrides { + self.values.insert(k.clone(), v.clone()); + } + } + + /// Return the effective value for a single key, or `None` if absent. + pub fn get(&self, key: &str) -> Option<&serde_json::Value> { + self.values.get(key) + } +} + +/// Resolve `{$vendor-dir}`, `{$home}`, `{$cache-dir}` placeholders inside +/// string values. Only one pass is performed (no recursive expansion). +pub fn resolve_references(config: &mut ComposerConfig) { + // Snapshot the values we need for substitution before mutating. + let vendor_dir = config + .values + .get("vendor-dir") + .and_then(|v| v.as_str()) + .unwrap_or("vendor") + .to_string(); + + let home = composer_home().to_string_lossy().into_owned(); + + let cache_dir = config + .values + .get("cache-dir") + .and_then(|v| v.as_str()) + .unwrap_or("{$home}/cache") + .replace("{$home}", &home); + + let replacements: &[(&str, &str)] = &[ + ("{$vendor-dir}", &vendor_dir), + ("{$home}", &home), + ("{$cache-dir}", &cache_dir), + ]; + + let keys: Vec<String> = config.values.keys().cloned().collect(); + for key in keys { + if let Some(serde_json::Value::String(s)) = config.values.get(&key).cloned() { + let mut resolved = s.clone(); + for (placeholder, replacement) in replacements { + resolved = resolved.replace(placeholder, replacement); + } + if resolved != s { + config + .values + .insert(key, serde_json::Value::String(resolved)); + } + } + } +} + +// ─── Composer ──────────────────────────────────────────────────────────────── + +/// Project-level Composer state. Currently only carries the merged +/// `ComposerConfig`; additional accessors (root package, locker, …) can be +/// layered on as commands need them. +pub struct Composer { + project_dir: PathBuf, + config: ComposerConfig, +} + +impl Composer { + /// Load Composer state for `project_dir`, requiring a composer.json. + /// Mirrors `BaseCommand::requireComposer()`. + pub fn require(project_dir: impl Into<PathBuf>) -> anyhow::Result<Self> { + let project_dir = project_dir.into(); + let composer_json = project_dir.join("composer.json"); + if !composer_json.exists() { + anyhow::bail!( + "Composer could not find a composer.json file in {}", + project_dir.display() + ); + } + Self::load(project_dir, &composer_json) + } + + /// Load Composer state for `project_dir`, returning `None` if no + /// composer.json exists. Other I/O or parse errors still propagate. + /// Mirrors `BaseCommand::tryComposer()`. + pub fn try_load(project_dir: impl Into<PathBuf>) -> anyhow::Result<Option<Self>> { + let project_dir = project_dir.into(); + let composer_json = project_dir.join("composer.json"); + if !composer_json.exists() { + return Ok(None); + } + Self::load(project_dir, &composer_json).map(Some) + } + + fn load(project_dir: PathBuf, composer_json: &Path) -> anyhow::Result<Self> { + let content = std::fs::read_to_string(composer_json)?; + let value: serde_json::Value = serde_json::from_str(&content)?; + let mut config = ComposerConfig::defaults(); + if let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) { + let overrides: BTreeMap<String, serde_json::Value> = cfg_obj + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + config.merge(&overrides); + } + resolve_references(&mut config); + Ok(Self { + project_dir, + config, + }) + } + + pub fn project_dir(&self) -> &Path { + &self.project_dir + } + + pub fn config(&self) -> &ComposerConfig { + &self.config + } +} diff --git a/crates/mozart-core/src/lib.rs b/crates/mozart-core/src/lib.rs index bc0da53..5e51d63 100644 --- a/crates/mozart-core/src/lib.rs +++ b/crates/mozart-core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod composer; pub mod console; pub mod exit_code; pub mod http; diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs index faa0e94..ca57259 100644 --- a/crates/mozart/src/commands/archive.rs +++ b/crates/mozart/src/commands/archive.rs @@ -104,24 +104,26 @@ pub async fn execute( None => std::env::current_dir()?, }; - // 2. Load config for format/dir defaults from composer.json's "config" section + // 2. Load Composer state for format/dir defaults. Composer's + // `archive` command falls back to `Factory::createConfig()` when no + // composer.json is present, so we mirror that by treating a missing + // file as "use defaults" (Composer::try_load returns None). + let composer = mozart_core::composer::Composer::try_load(&working_dir)?; let composer_json_path = working_dir.join("composer.json"); - let (config_archive_format, config_archive_dir) = if composer_json_path.exists() { - let content = std::fs::read_to_string(&composer_json_path)?; - let value: serde_json::Value = serde_json::from_str(&content)?; - let fmt = value - .get("config") - .and_then(|c| c.get("archive-format")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let dir = value - .get("config") - .and_then(|c| c.get("archive-dir")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - (fmt, dir) - } else { - (None, None) + let (config_archive_format, config_archive_dir) = match composer.as_ref() { + Some(c) => { + let cfg = c.config(); + let fmt = cfg + .get("archive-format") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let dir = cfg + .get("archive-dir") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + (fmt, dir) + } + None => (None, None), }; // 3. Determine format: args -> config -> default "tar" diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 3947afd..11b1b7b 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -63,124 +63,7 @@ pub struct ConfigArgs { // ─── ComposerConfig ─────────────────────────────────────────────────────────── -/// Holds the effective configuration key-value pairs for a project. -/// Keys mirror Composer's `Config.php` defaults. -pub struct ComposerConfig { - pub values: BTreeMap<String, serde_json::Value>, -} - -impl ComposerConfig { - /// Build a `ComposerConfig` starting from the built-in defaults. - pub fn defaults() -> Self { - let mut m: BTreeMap<String, serde_json::Value> = BTreeMap::new(); - - m.insert("process-timeout".to_string(), serde_json::json!(300)); - m.insert("use-include-path".to_string(), serde_json::json!(false)); - m.insert("preferred-install".to_string(), serde_json::json!("dist")); - m.insert("notify-on-install".to_string(), serde_json::json!(true)); - m.insert( - "github-protocols".to_string(), - serde_json::json!(["https", "ssh", "git"]), - ); - m.insert("vendor-dir".to_string(), serde_json::json!("vendor")); - m.insert( - "bin-dir".to_string(), - serde_json::json!("{$vendor-dir}/bin"), - ); - m.insert("bin-compat".to_string(), serde_json::json!("auto")); - m.insert("cache-dir".to_string(), serde_json::json!("{$home}/cache")); - m.insert( - "cache-files-dir".to_string(), - serde_json::json!("{$cache-dir}/files"), - ); - m.insert( - "cache-repo-dir".to_string(), - serde_json::json!("{$cache-dir}/repo"), - ); - m.insert( - "cache-vcs-dir".to_string(), - serde_json::json!("{$cache-dir}/vcs"), - ); - m.insert("cache-files-ttl".to_string(), serde_json::json!(15_552_000)); - m.insert( - "cache-files-maxsize".to_string(), - serde_json::json!("300MiB"), - ); - m.insert("cache-read-only".to_string(), serde_json::json!(false)); - m.insert("prepend-autoloader".to_string(), serde_json::json!(true)); - m.insert("autoloader-suffix".to_string(), serde_json::Value::Null); - m.insert("optimize-autoloader".to_string(), serde_json::json!(false)); - m.insert("sort-packages".to_string(), serde_json::json!(false)); - m.insert( - "classmap-authoritative".to_string(), - serde_json::json!(false), - ); - m.insert("apcu-autoloader".to_string(), serde_json::json!(false)); - m.insert("platform".to_string(), serde_json::json!({})); - m.insert("platform-check".to_string(), serde_json::json!("php-only")); - m.insert("lock".to_string(), serde_json::json!(true)); - m.insert("discard-changes".to_string(), serde_json::json!(false)); - m.insert("archive-format".to_string(), serde_json::json!("tar")); - m.insert("archive-dir".to_string(), serde_json::json!(".")); - m.insert("htaccess-protect".to_string(), serde_json::json!(true)); - m.insert("secure-http".to_string(), serde_json::json!(true)); - m.insert("allow-plugins".to_string(), serde_json::json!({})); - - Self { values: m } - } - - /// Merge `overrides` on top of the current values. - pub fn merge(&mut self, overrides: &BTreeMap<String, serde_json::Value>) { - for (k, v) in overrides { - self.values.insert(k.clone(), v.clone()); - } - } - - /// Resolve `{$vendor-dir}`, `{$home}`, `{$cache-dir}` placeholders inside - /// string values. Only one pass is performed (no recursive expansion). - pub fn resolve_references(&mut self) { - // Snapshot the values we need for substitution before mutating. - let vendor_dir = self - .values - .get("vendor-dir") - .and_then(|v| v.as_str()) - .unwrap_or("vendor") - .to_string(); - - let home = composer_home().to_string_lossy().into_owned(); - - let cache_dir = self - .values - .get("cache-dir") - .and_then(|v| v.as_str()) - .unwrap_or("{$home}/cache") - .replace("{$home}", &home); - - let replacements: &[(&str, &str)] = &[ - ("{$vendor-dir}", &vendor_dir), - ("{$home}", &home), - ("{$cache-dir}", &cache_dir), - ]; - - let keys: Vec<String> = self.values.keys().cloned().collect(); - for key in keys { - if let Some(serde_json::Value::String(s)) = self.values.get(&key).cloned() { - let mut resolved = s.clone(); - for (placeholder, replacement) in replacements { - resolved = resolved.replace(placeholder, replacement); - } - if resolved != s { - self.values.insert(key, serde_json::Value::String(resolved)); - } - } - } - } - - /// Return the effective value for a single key, or `None` if absent. - pub fn get(&self, key: &str) -> Option<&serde_json::Value> { - self.values.get(key) - } -} +pub use mozart_core::composer::{ComposerConfig, resolve_references}; // ─── ConfigValueType ───────────────────────────────────────────────────────── @@ -911,7 +794,7 @@ fn execute_read( config.merge(&overrides); } - config.resolve_references(); + resolve_references(&mut config); // If --absolute is requested, resolve *-dir values to absolute paths. if args.absolute { @@ -1120,7 +1003,7 @@ mod tests { fn test_reference_resolution_bin_dir() { let mut cfg = ComposerConfig::defaults(); // bin-dir default is "{$vendor-dir}/bin"; vendor-dir default is "vendor" - cfg.resolve_references(); + resolve_references(&mut cfg); assert_eq!(cfg.values["bin-dir"], serde_json::json!("vendor/bin")); } @@ -1132,7 +1015,7 @@ mod tests { // Override vendor-dir before resolving cfg.values .insert("vendor-dir".to_string(), serde_json::json!("lib")); - cfg.resolve_references(); + resolve_references(&mut cfg); assert_eq!(cfg.values["bin-dir"], serde_json::json!("lib/bin")); } @@ -1145,7 +1028,7 @@ mod tests { "cache-dir".to_string(), serde_json::json!("/home/user/.cache/composer"), ); - cfg.resolve_references(); + resolve_references(&mut cfg); assert_eq!( cfg.values["cache-files-dir"], @@ -1165,7 +1048,7 @@ mod tests { fn test_reference_resolution_no_change_for_non_string() { let mut cfg = ComposerConfig::defaults(); let before = cfg.values["process-timeout"].clone(); - cfg.resolve_references(); + resolve_references(&mut cfg); // Numeric values should be untouched. assert_eq!(cfg.values["process-timeout"], before); } @@ -1280,7 +1163,7 @@ mod tests { let overrides = load_config_section(&composer_json).unwrap(); let mut cfg = ComposerConfig::defaults(); cfg.merge(&overrides); - cfg.resolve_references(); + resolve_references(&mut cfg); assert_eq!(cfg.values["vendor-dir"], serde_json::json!("custom_vendor")); assert_eq!(cfg.values["sort-packages"], serde_json::json!(true)); diff --git a/crates/mozart/src/commands/config_helpers.rs b/crates/mozart/src/commands/config_helpers.rs index 422db4d..2f856a7 100644 --- a/crates/mozart/src/commands/config_helpers.rs +++ b/crates/mozart/src/commands/config_helpers.rs @@ -1,64 +1,7 @@ use anyhow::anyhow; use std::path::{Path, PathBuf}; -/// Return the Composer home directory, respecting `COMPOSER_HOME` and -/// falling back to the platform default using Composer-compatible logic. -/// -/// On Unix: -/// - If XDG is in use (any `XDG_*` env var exists, or `/etc/xdg` exists), -/// prefer `$XDG_CONFIG_HOME/composer` (or `$HOME/.config/composer`). -/// - Always include `$HOME/.composer` as a fallback candidate. -/// - Return the first candidate directory that exists on disk; -/// if none exist, return the first candidate. -pub(crate) fn composer_home() -> PathBuf { - if let Ok(val) = std::env::var("COMPOSER_HOME") - && !val.is_empty() - { - return PathBuf::from(val); - } - - #[cfg(target_os = "windows")] - { - if let Ok(appdata) = std::env::var("APPDATA") - && !appdata.is_empty() - { - return PathBuf::from(appdata).join("Composer"); - } - return PathBuf::from("C:/ProgramData/ComposerSetup/bin"); - } - - #[cfg(not(target_os = "windows"))] - { - let home_dir = std::env::var("HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/tmp")); - - let mut candidates: Vec<PathBuf> = Vec::new(); - - if use_xdg() { - let xdg_config = std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home_dir.join(".config")); - candidates.push(xdg_config.join("composer")); - } - - candidates.push(home_dir.join(".composer")); - - // Return first candidate that exists; otherwise return the first - candidates - .iter() - .find(|p| p.is_dir()) - .cloned() - .unwrap_or_else(|| candidates.into_iter().next().unwrap()) - } -} - -/// Check whether XDG base directories are in use: -/// any env var starting with `XDG_` exists, OR `/etc/xdg` directory exists. -fn use_xdg() -> bool { - std::env::vars().any(|(k, _)| k.starts_with("XDG_")) - || std::path::Path::new("/etc/xdg").is_dir() -} +pub(crate) use mozart_core::composer::composer_home; /// Build the working directory path, preferring `--working-dir` over `cwd`. pub(crate) fn working_dir(cli: &super::Cli) -> anyhow::Result<PathBuf> { diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs index c35690b..ec25b54 100644 --- a/crates/mozart/src/commands/dump_autoload.rs +++ b/crates/mozart/src/commands/dump_autoload.rs @@ -66,20 +66,9 @@ pub async fn execute( let vendor_dir = working_dir.join("vendor"); let dev_mode = !args.no_dev; - // B: Read config-driven defaults from composer.json - let composer_json_path = working_dir.join("composer.json"); - let mut composer_config = super::config::ComposerConfig::defaults(); - if composer_json_path.exists() - && let Ok(content) = std::fs::read_to_string(&composer_json_path) - && let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) - && let Some(cfg_obj) = value.get("config").and_then(|v| v.as_object()) - { - let overrides: std::collections::BTreeMap<String, serde_json::Value> = cfg_obj - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - composer_config.merge(&overrides); - } + // B: Load Composer state (composer.json is required for dump-autoload) + let composer = mozart_core::composer::Composer::require(&working_dir)?; + let composer_config = composer.config(); let optimize = args.optimize || composer_config diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index eaaf465..5f1f0bc 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::composer::Composer; use mozart_core::console_format; use std::path::{Path, PathBuf}; @@ -29,7 +30,9 @@ pub async fn execute( None => std::env::current_dir()?, }; - let bin_dir = resolve_bin_dir(&working_dir); + // ExecCommand uses requireComposer in Composer; composer.json must exist. + let composer = Composer::require(&working_dir)?; + let bin_dir = resolve_bin_dir(&working_dir, &composer); if args.list || args.binary.is_none() { let binaries = get_binaries(&working_dir, &bin_dir); @@ -119,20 +122,14 @@ pub async fn execute( // ─── Helpers ────────────────────────────────────────────────────────────────── -fn resolve_bin_dir(working_dir: &Path) -> PathBuf { - let composer_json_path = working_dir.join("composer.json"); - if let Ok(content) = std::fs::read_to_string(&composer_json_path) - && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) - { - let vendor_dir = parsed["config"]["vendor-dir"].as_str().unwrap_or("vendor"); - let bin_dir = parsed["config"]["bin-dir"].as_str().unwrap_or_default(); - if !bin_dir.is_empty() { - let resolved = bin_dir.replace("{$vendor-dir}", vendor_dir); - return working_dir.join(resolved); - } - return working_dir.join(vendor_dir).join("bin"); - } - working_dir.join("vendor/bin") +fn resolve_bin_dir(working_dir: &Path, composer: &Composer) -> PathBuf { + // bin-dir's `{$vendor-dir}` placeholder is already resolved by Composer::load. + let bin_dir = composer + .config() + .get("bin-dir") + .and_then(|v| v.as_str()) + .unwrap_or("vendor/bin"); + working_dir.join(bin_dir) } /// Returns a vec of (name, is_local) tuples for all available binaries. @@ -208,7 +205,8 @@ mod tests { let composer_json = dir.path().join("composer.json"); fs::write(&composer_json, r#"{"name": "test/pkg", "require": {}}"#).unwrap(); - let result = resolve_bin_dir(dir.path()); + let composer = Composer::require(dir.path()).unwrap(); + let result = resolve_bin_dir(dir.path(), &composer); assert_eq!(result, dir.path().join("vendor/bin")); } @@ -222,7 +220,8 @@ mod tests { ) .unwrap(); - let result = resolve_bin_dir(dir.path()); + let composer = Composer::require(dir.path()).unwrap(); + let result = resolve_bin_dir(dir.path(), &composer); assert_eq!(result, dir.path().join("libs/bin")); } @@ -236,7 +235,8 @@ mod tests { ) .unwrap(); - let result = resolve_bin_dir(dir.path()); + let composer = Composer::require(dir.path()).unwrap(); + let result = resolve_bin_dir(dir.path(), &composer); assert_eq!(result, dir.path().join("scripts")); } @@ -250,7 +250,8 @@ mod tests { ) .unwrap(); - let result = resolve_bin_dir(dir.path()); + let composer = Composer::require(dir.path()).unwrap(); + let result = resolve_bin_dir(dir.path(), &composer); assert_eq!(result, dir.path().join("packages/commands")); } @@ -366,7 +367,8 @@ mod tests { ) .unwrap(); - let bin_dir = resolve_bin_dir(dir.path()); + let composer = Composer::require(dir.path()).unwrap(); + let bin_dir = resolve_bin_dir(dir.path(), &composer); // No binaries exist — looking up a name should find nothing let candidate = bin_dir.join("nonexistent-binary"); diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs index 2d88a81..e57d1c2 100644 --- a/crates/mozart/src/commands/run_script.rs +++ b/crates/mozart/src/commands/run_script.rs @@ -84,6 +84,9 @@ pub async fn execute( None => std::env::current_dir()?, }; + // RunScriptCommand uses requireComposer in Composer; composer.json must exist. + let composer = mozart_core::composer::Composer::require(&working_dir)?; + let (scripts, descriptions) = load_scripts(&working_dir)?; if args.list { @@ -114,21 +117,12 @@ pub async fn execute( let timeout = match args.timeout { Some(0) => None, Some(secs) => Some(Duration::from_secs(secs)), - None => { - let composer_json_path = working_dir.join("composer.json"); - if let Ok(content) = std::fs::read_to_string(&composer_json_path) - && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) - && let Some(secs) = parsed["config"]["process-timeout"].as_u64() - { - if secs == 0 { - None - } else { - Some(Duration::from_secs(secs)) - } - } else { - Some(Duration::from_secs(300)) - } - } + None => composer + .config() + .get("process-timeout") + .and_then(|v| v.as_u64()) + .filter(|s| *s != 0) + .map(Duration::from_secs), }; let dev_mode = !args.no_dev; @@ -138,7 +132,7 @@ pub async fn execute( std::env::set_var("COMPOSER_DEV_MODE", if dev_mode { "1" } else { "0" }); } - let bin_dir = resolve_bin_dir(&working_dir); + let bin_dir = resolve_bin_dir(&working_dir, &composer); let mut event_stack: Vec<String> = Vec::new(); let exit_code = run_script( @@ -464,20 +458,14 @@ fn wait_with_timeout( // ─── Bin dir resolution ─────────────────────────────────────────────────────── -fn resolve_bin_dir(working_dir: &Path) -> PathBuf { - let composer_json_path = working_dir.join("composer.json"); - if let Ok(content) = std::fs::read_to_string(&composer_json_path) - && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) - { - let vendor_dir = parsed["config"]["vendor-dir"].as_str().unwrap_or("vendor"); - let bin_dir = parsed["config"]["bin-dir"].as_str().unwrap_or_default(); - if !bin_dir.is_empty() { - let resolved = bin_dir.replace("{$vendor-dir}", vendor_dir); - return working_dir.join(resolved); - } - return working_dir.join(vendor_dir).join("bin"); - } - working_dir.join("vendor/bin") +fn resolve_bin_dir(working_dir: &Path, composer: &mozart_core::composer::Composer) -> PathBuf { + // bin-dir's `{$vendor-dir}` placeholder is already resolved by Composer::load. + let bin_dir = composer + .config() + .get("bin-dir") + .and_then(|v| v.as_str()) + .unwrap_or("vendor/bin"); + working_dir.join(bin_dir) } // ─── Classifier functions ───────────────────────────────────────────────────── |
