Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 118 additions & 84 deletions rust/src/cli/pack_cmd/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = None;
let mut version = "1.0.0".to_string();
let mut description = String::new();
let mut author: Option<String> = None;
let mut tags: Vec<String> = Vec::new();
let mut layers_str: Option<String> = None;
let mut level: u32 = 1;
let mut scope: Option<String> = None;
let mut private = false;
let mut kind: Option<String> = None;
let mut from_dir: Option<String> = None;
#[derive(Default)]
struct PackCreateArgs {
name: Option<String>,
version: String,
description: String,
author: Option<String>,
tags: Vec<String>,
layers_str: Option<String>,
level: u32,
scope: Option<String>,
private: bool,
kind: Option<String>,
from_dir: Option<String>,
}

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<String> {
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::<u32>().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::<u32>().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::<u32>().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");
Expand Down
139 changes: 76 additions & 63 deletions rust/src/setup/with_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,81 +498,94 @@ pub fn run_setup_with_options(opts: SetupOptions) -> Result<SetupReport, String>
}
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<SetupStepReport>) {
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<Utc>,
steps: Vec<SetupStepReport>,
) -> 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,
Expand All @@ -584,13 +597,13 @@ pub fn run_setup_with_options(opts: SetupOptions) -> Result<SetupReport, String>
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)
}
Loading