diff --git a/rust/src/cli/pack_cmd/package.rs b/rust/src/cli/pack_cmd/package.rs index 7cd99864d3..f7adad8093 100644 --- a/rust/src/cli/pack_cmd/package.rs +++ b/rust/src/cli/pack_cmd/package.rs @@ -2,102 +2,136 @@ use std::path::PathBuf; use super::{format_bytes, parse_flag, parse_pkg_ref}; -pub(super) fn cmd_pack_create(args: &[String], project_root: &str) { - let mut name: Option = None; - let mut version = "1.0.0".to_string(); - let mut description = String::new(); - let mut author: Option = None; - let mut tags: Vec = Vec::new(); - let mut layers_str: Option = None; - let mut level: u32 = 1; - let mut scope: Option = None; - let mut private = false; - let mut kind: Option = None; - let mut from_dir: Option = None; +#[derive(Default)] +struct PackCreateArgs { + name: Option, + version: String, + description: String, + author: Option, + tags: Vec, + layers_str: Option, + level: u32, + scope: Option, + private: bool, + kind: Option, + from_dir: Option, +} + +fn parse_pack_create_args(args: &[String]) -> PackCreateArgs { + let mut parsed = PackCreateArgs { + version: "1.0.0".to_string(), + level: 1, + ..Default::default() + }; let mut i = 0; while i < args.len() { - let a = &args[i]; - if a == "create" { - i += 1; - continue; - } - if a == "--private" { - private = true; - i += 1; - continue; - } - if let Some(v) = a.strip_prefix("--kind=") { - kind = Some(v.to_string()); - i += 1; - continue; + i += parse_pack_create_option(&mut parsed, args, i); + } + + parsed +} + +fn flag_value(args: &[String], index: usize) -> Option { + args.get(index).filter(|v| !v.starts_with("--")).cloned() +} + +fn parse_pack_create_option(parsed: &mut PackCreateArgs, args: &[String], index: usize) -> usize { + let arg = &args[index]; + match arg.as_str() { + "create" => 1, + "--private" => { + parsed.private = true; + 1 } - if a == "--kind" { - i += 1; - kind = args.get(i).filter(|v| !v.starts_with("--")).cloned(); - i += 1; - continue; + "--kind" => { + parsed.kind = flag_value(args, index + 1); + 2 } - if let Some(v) = a.strip_prefix("--from=") { - from_dir = Some(v.to_string()); - i += 1; - continue; + "--from" => { + parsed.from_dir = flag_value(args, index + 1); + 2 } - if a == "--from" { - i += 1; - from_dir = args.get(i).filter(|v| !v.starts_with("--")).cloned(); - i += 1; - continue; + "--name" => { + parsed.name = flag_value(args, index + 1); + 2 } - if let Some(v) = a.strip_prefix("--name=") { - name = Some(v.to_string()); - } else if a == "--name" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - name = Some(v.clone()); + "--version" => { + if let Some(value) = flag_value(args, index + 1) { + parsed.version = value; } - } else if let Some(v) = a.strip_prefix("--version=") { - version = v.to_string(); - } else if a == "--version" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - v.clone_into(&mut version); - } - } else if let Some(v) = a.strip_prefix("--description=") { - description = v.to_string(); - } else if a == "--description" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - v.clone_into(&mut description); - } - } else if let Some(v) = a.strip_prefix("--author=") { - author = Some(v.to_string()); - } else if a == "--author" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - author = Some(v.clone()); - } - } else if let Some(v) = a.strip_prefix("--tags=") { - tags = v.split(',').map(|s| s.trim().to_string()).collect(); - } else if let Some(v) = a.strip_prefix("--layers=") { - layers_str = Some(v.to_string()); - } else if let Some(v) = a.strip_prefix("--level=") { - level = v.parse::().unwrap_or(1).clamp(1, 3); - } else if a == "--level" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - level = v.parse::().unwrap_or(1).clamp(1, 3); + 2 + } + "--description" => { + if let Some(value) = flag_value(args, index + 1) { + parsed.description = value; } - } else if let Some(v) = a.strip_prefix("--scope=") { - scope = Some(v.to_string()); - } else if a == "--scope" { - i += 1; - if let Some(v) = args.get(i).filter(|v| !v.starts_with("--")) { - scope = Some(v.clone()); + 2 + } + "--author" => { + parsed.author = flag_value(args, index + 1); + 2 + } + "--level" => { + if let Some(value) = flag_value(args, index + 1) { + parsed.level = parse_pack_level(&value); } + 2 } - i += 1; + "--scope" => { + parsed.scope = flag_value(args, index + 1); + 2 + } + _ => { + parse_pack_create_inline_option(parsed, arg); + 1 + } + } +} + +fn parse_pack_create_inline_option(parsed: &mut PackCreateArgs, arg: &str) { + if let Some(value) = arg.strip_prefix("--kind=") { + parsed.kind = Some(value.to_string()); + } else if let Some(value) = arg.strip_prefix("--from=") { + parsed.from_dir = Some(value.to_string()); + } else if let Some(value) = arg.strip_prefix("--name=") { + parsed.name = Some(value.to_string()); + } else if let Some(value) = arg.strip_prefix("--version=") { + parsed.version = value.to_string(); + } else if let Some(value) = arg.strip_prefix("--description=") { + parsed.description = value.to_string(); + } else if let Some(value) = arg.strip_prefix("--author=") { + parsed.author = Some(value.to_string()); + } else if let Some(value) = arg.strip_prefix("--tags=") { + parsed.tags = value.split(',').map(|s| s.trim().to_string()).collect(); + } else if let Some(value) = arg.strip_prefix("--layers=") { + parsed.layers_str = Some(value.to_string()); + } else if let Some(value) = arg.strip_prefix("--level=") { + parsed.level = parse_pack_level(value); + } else if let Some(value) = arg.strip_prefix("--scope=") { + parsed.scope = Some(value.to_string()); } +} + +fn parse_pack_level(value: &str) -> u32 { + value.parse::().unwrap_or(1).clamp(1, 3) +} + +pub(super) fn cmd_pack_create(args: &[String], project_root: &str) { + let parsed = parse_pack_create_args(args); + let PackCreateArgs { + name, + version, + description, + author, + tags, + layers_str, + level, + scope, + private, + kind, + from_dir, + } = parsed; let Some(pkg_name) = name else { eprintln!("ERROR: --name is required for pack create"); diff --git a/rust/src/setup/with_options.rs b/rust/src/setup/with_options.rs index 65a3e075b5..6772d7cc48 100644 --- a/rust/src/setup/with_options.rs +++ b/rust/src/setup/with_options.rs @@ -498,81 +498,94 @@ pub fn run_setup_with_options(opts: SetupOptions) -> Result } steps.push(env_step); - // Project root validation: warn if no root is configured and cwd is broad - { - let has_env_root = std::env::var("LEAN_CTX_PROJECT_ROOT").is_ok_and(|v| !v.is_empty()); - let cfg = crate::core::config::Config::load(); - let has_cfg_root = cfg.project_root.as_ref().is_some_and(|v| !v.is_empty()); - if !has_env_root - && !has_cfg_root - && let Ok(cwd) = std::env::current_dir() - { - let is_home = dirs::home_dir().is_some_and(|h| cwd == h); - if is_home { - let mut root_step = SetupStepReport { - name: "project_root".to_string(), - ok: true, - items: Vec::new(), - warnings: vec![ - "No project_root configured. Running from $HOME can cause excessive scanning. \ - Set via: lean-ctx config set project_root /path/to/project".to_string() - ], - errors: Vec::new(), - }; - root_step.items.push(SetupItem { - name: "project_root".to_string(), - status: "unconfigured".to_string(), - path: None, - note: Some( - "Set LEAN_CTX_PROJECT_ROOT or add project_root to config.toml".to_string(), - ), - }); - steps.push(root_step); - } - } + maybe_add_project_root_warning(&mut steps); + maybe_spawn_background_index(); + maybe_enable_ide_config_access(opts); + + let report = build_setup_report(started_at, steps); + persist_setup_report(&report)?; + + Ok(report) +} + +fn maybe_add_project_root_warning(steps: &mut Vec) { + let has_env_root = std::env::var("LEAN_CTX_PROJECT_ROOT").is_ok_and(|v| !v.is_empty()); + let cfg = crate::core::config::Config::load(); + let has_cfg_root = cfg.project_root.as_ref().is_some_and(|v| !v.is_empty()); + + if has_env_root || has_cfg_root { + return; } - // Auto-build property graph if inside any recognized project. The marker - // probe is TCC-guarded (#356): a launchd-standalone setup run never stats - // markers under ~/Documents — and `may_autoindex_cwd` additionally skips the - // probe for a non-standalone CLI refresh whose cwd is in a protected dir. + let Ok(cwd) = std::env::current_dir() else { + return; + }; + let is_home = dirs::home_dir().is_some_and(|h| cwd == h); + if !is_home { + return; + } + + let mut root_step = SetupStepReport { + name: "project_root".to_string(), + ok: true, + items: Vec::new(), + warnings: vec![ + "No project_root configured. Running from $HOME can cause excessive scanning. \ + Set via: lean-ctx config set project_root /path/to/project" + .to_string(), + ], + errors: Vec::new(), + }; + root_step.items.push(SetupItem { + name: "project_root".to_string(), + status: "unconfigured".to_string(), + path: None, + note: Some("Set LEAN_CTX_PROJECT_ROOT or add project_root to config.toml".to_string()), + }); + steps.push(root_step); +} + +fn maybe_spawn_background_index() { if let Ok(cwd) = std::env::current_dir() && may_autoindex_cwd(&cwd) && crate::core::pathutil::has_project_marker(&cwd) { spawn_index_build_background(&cwd); } +} - // IDE config access: the interactive `setup` prompts for informed consent - // (see run_setup). An explicit `--yes` is itself consent, so enable the - // registry-derived opt-in if the user has never decided. `--fix` repair runs - // must never silently widen the jail, so they are left untouched. - if opts.yes - && !opts.fix - && crate::core::config::Config::load() +fn maybe_enable_ide_config_access(opts: SetupOptions) { + if !opts.yes + || opts.fix + || crate::core::config::Config::load() .allow_ide_config_dirs - .is_none() + .is_some() { - match crate::core::config::Config::update_global(|c| { - c.allow_ide_config_dirs = Some(true); - }) { - // --yes is consent, but say so out loud: the user should know the - // path jail now includes IDE config dirs, and how to revert it. - Ok(_) => { - if !opts.json { - println!( - " Enabled IDE config access (allow_ide_config_dirs) — \ - disable: lean-ctx config set allow_ide_config_dirs false" - ); - } + return; + } + + match crate::core::config::Config::update_global(|c| { + c.allow_ide_config_dirs = Some(true); + }) { + Ok(_) => { + if !opts.json { + println!( + " Enabled IDE config access (allow_ide_config_dirs) — \ + disable: lean-ctx config set allow_ide_config_dirs false" + ); } - Err(e) => tracing::warn!("could not enable IDE config access: {e}"), } + Err(e) => tracing::warn!("could not enable IDE config access: {e}"), } +} +fn build_setup_report( + started_at: chrono::DateTime, + steps: Vec, +) -> SetupReport { let finished_at = Utc::now(); let success = steps.iter().all(|s| s.ok); - let report = SetupReport { + SetupReport { schema_version: 1, started_at, finished_at, @@ -584,13 +597,13 @@ pub fn run_setup_with_options(opts: SetupOptions) -> Result steps, warnings: Vec::new(), errors: Vec::new(), - }; + } +} +fn persist_setup_report(report: &SetupReport) -> Result<(), String> { let path = SetupReport::default_path()?; let mut content = - serde_json::to_string_pretty(&report).map_err(|e| format!("serialize report: {e}"))?; + serde_json::to_string_pretty(report).map_err(|e| format!("serialize report: {e}"))?; content.push('\n'); - crate::config_io::write_atomic(&path, &content)?; - - Ok(report) + crate::config_io::write_atomic(&path, &content) }