1use clap::Subcommand;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use nika_engine::error::NikaError;
8use nika_engine::serde_yaml;
9
10#[derive(Subcommand)]
14pub enum PkgAction {
15 List {
17 #[arg(long)]
19 json: bool,
20 },
21
22 Info {
24 package: String,
26 },
27
28 Add {
33 package: String,
35
36 #[arg(short, long)]
38 r#type: Option<String>,
39
40 #[arg(long, visible_alias = "ver")]
42 version: Option<String>,
43
44 #[arg(long)]
46 dev: bool,
47 },
48
49 Remove {
51 package: String,
53
54 #[arg(short, long)]
56 yes: bool,
57 },
58
59 Install {
61 #[arg(long)]
63 frozen: bool,
64 },
65
66 Update {
68 package: Option<String>,
70 },
71
72 Outdated,
74
75 Search {
77 query: String,
79
80 #[arg(short, long)]
82 r#type: Option<String>,
83
84 #[arg(short, long, default_value = "20")]
86 limit: usize,
87 },
88}
89
90pub async fn handle_pkg_command(action: PkgAction) -> Result<(), NikaError> {
93 use colored::Colorize;
94 use nika_engine::registry::{list_installed, load_manifest, load_registry};
95
96 match action {
97 PkgAction::List { json } => {
98 let packages = list_installed()?;
99
100 if json {
101 let registry = load_registry()?;
102 println!("{}", serde_json::to_string_pretty(®istry)?);
103 } else if packages.is_empty() {
104 println!("{} No packages installed", "ℹ".cyan());
105 println!();
106 println!("Install packages with:");
107 println!(" nika pkg add @nika/seo-audit");
108 println!(" nika pkg install # Install from nika.yaml");
109 } else {
110 println!("{}", "Installed Packages".bold());
111 println!("{}", "─".repeat(60));
112
113 for (name, version) in &packages {
114 println!(" {}@{}", name.cyan(), version.green());
115 }
116
117 println!();
118 println!("{} package(s) installed", packages.len());
119 }
120 Ok(())
121 }
122
123 PkgAction::Info { package } => {
124 let registry = load_registry()?;
126
127 if let Some(installed) = registry.get(&package) {
128 println!("{}", format!("Package: {package}").bold());
129 println!("{}", "─".repeat(60));
130 println!(" Version: {}", installed.version.green());
131 println!(" Path: {}", installed.manifest_path.dimmed());
132 println!(" Installed: {}", installed.installed_at.dimmed());
133
134 if let Ok(manifest) = load_manifest(&package, &installed.version) {
136 if let Some(ref desc) = manifest.description {
137 println!(" Description: {desc}");
138 }
139 if !manifest.skills.is_empty() {
140 println!();
141 println!(" Skills:");
142 for (name, skill) in &manifest.skills {
143 println!(" • {} ({})", name.cyan(), skill.path.dimmed());
144 }
145 }
146 }
147 } else {
148 println!("{} Package '{}' not installed", "ℹ".cyan(), package);
149 println!();
150 println!("To install: nika pkg add {package}");
151 }
152 Ok(())
153 }
154
155 PkgAction::Add {
156 package,
157 r#type,
158 version,
159 dev: _dev,
160 } => {
161 use nika_engine::registry::{
162 ensure_nika_home, is_version_installed, package_dir, save_registry,
163 InstalledPackage, RegistryClient,
164 };
165
166 println!("{} Adding package: {}", "📦".cyan(), package.green());
167
168 let pkg_type = r#type
170 .as_deref()
171 .or_else(|| infer_package_type(&package))
172 .unwrap_or("workflow");
173
174 println!(" Type: {}", pkg_type.dimmed());
175
176 ensure_nika_home()?;
178
179 let client = RegistryClient::new().map_err(|e| NikaError::ConfigError {
181 reason: format!("Failed to create registry client: {e}"),
182 })?;
183
184 println!(" {} Fetching package info...", "→".dimmed());
186 let pkg_info =
187 client
188 .get_package(&package)
189 .await
190 .map_err(|_e| NikaError::PackageNotFound {
191 name: package.clone(),
192 version: "latest".to_string(),
193 })?;
194
195 let target_version = version.as_deref().unwrap_or(&pkg_info.latest_version);
197 println!(" {} Version: {}", "→".dimmed(), target_version.green());
198
199 if is_version_installed(&package, target_version)? {
201 println!(
202 "{} {}@{} is already installed",
203 "✓".green(),
204 package.cyan(),
205 target_version.green()
206 );
207 return Ok(());
208 }
209
210 let target_dir = package_dir(&package, target_version)?;
212 println!(
213 " {} Installing to: {}",
214 "→".dimmed(),
215 target_dir.display().to_string().dimmed()
216 );
217
218 println!(" {} Downloading...", "→".dimmed());
220 client
221 .download_and_extract(&package, target_version, &target_dir)
222 .await
223 .map_err(|e| NikaError::ValidationError {
224 reason: format!("Failed to download package: {e}"),
225 })?;
226
227 let mut registry = load_registry()?;
229 let manifest_path = format!("packages/{package}/{target_version}/manifest.yaml");
230 registry.insert(
231 package.clone(),
232 InstalledPackage::now(target_version.to_string(), manifest_path),
233 );
234 save_registry(®istry)?;
235
236 println!();
237 println!(
238 "{} Successfully installed {}@{}",
239 "✓".green(),
240 package.cyan(),
241 target_version.green()
242 );
243
244 if let Ok(manifest) = load_manifest(&package, target_version) {
246 if !manifest.skills.is_empty() {
247 println!();
248 println!(" {} Skills:", "📚".cyan());
249 for (name, skill) in &manifest.skills {
250 println!(" • {} ({})", name.cyan(), skill.path.dimmed());
251 }
252 }
253 }
254
255 Ok(())
256 }
257
258 PkgAction::Remove { package, yes: _ } => {
259 use nika_engine::registry::{package_dir, save_registry};
260
261 println!("{} Removing package: {}", "🗑".red(), package);
262
263 let mut registry = load_registry()?;
265 let installed = match registry.get(&package) {
266 Some(pkg) => pkg.clone(),
267 None => {
268 println!("{} Package '{}' is not installed", "ℹ".cyan(), package);
269 return Ok(());
270 }
271 };
272
273 let pkg_dir = package_dir(&package, &installed.version)?;
275
276 if pkg_dir.exists() {
278 println!(
279 " {} Removing {}",
280 "→".dimmed(),
281 pkg_dir.display().to_string().dimmed()
282 );
283 std::fs::remove_dir_all(&pkg_dir).map_err(|e| NikaError::ValidationError {
284 reason: format!("Failed to remove package directory: {e}"),
285 })?;
286
287 if let Some(parent) = pkg_dir.parent() {
289 if parent
290 .read_dir()
291 .map(|mut d| d.next().is_none())
292 .unwrap_or(false)
293 {
294 let _ = std::fs::remove_dir(parent);
295 }
296 }
297 }
298
299 registry.remove(&package);
301 save_registry(®istry)?;
302
303 println!();
304 println!(
305 "{} Successfully removed {}@{}",
306 "✓".green(),
307 package.cyan(),
308 installed.version.green()
309 );
310 Ok(())
311 }
312
313 PkgAction::Install { frozen } => {
314 use nika_engine::registry::{
315 ensure_nika_home, is_version_installed, package_dir, save_registry,
316 InstalledPackage, Lockfile, RegistryClient,
317 };
318
319 println!(
320 "{} Installing packages from project manifest{}",
321 "📦".cyan(),
322 if frozen { " (frozen)" } else { "" }
323 );
324
325 ensure_nika_home()?;
327
328 let manifest_path = if Path::new("nika.yaml").exists() {
330 PathBuf::from("nika.yaml")
331 } else {
332 println!("{} No project manifest found (nika.yaml)", "⚠".yellow());
333 println!();
334 println!("Create one with:");
335 println!(" nika init");
336 return Ok(());
337 };
338
339 println!(" {} Reading {}", "→".dimmed(), manifest_path.display());
340
341 let content =
343 fs::read_to_string(&manifest_path).map_err(|e| NikaError::ValidationError {
344 reason: format!("Failed to read {}: {}", manifest_path.display(), e),
345 })?;
346
347 #[derive(serde::Deserialize)]
349 struct ProjectManifest {
350 #[serde(default)]
351 dependencies: std::collections::HashMap<String, String>,
352 }
353
354 let manifest: ProjectManifest =
355 serde_yaml::from_str(&content).map_err(|e| NikaError::ParseError {
356 details: format!("Failed to parse manifest: {e}"),
357 })?;
358
359 if manifest.dependencies.is_empty() {
360 println!("{} No dependencies to install", "ℹ".cyan());
361 return Ok(());
362 }
363
364 let lockfile = if frozen {
366 println!(" {} Reading nika.lock", "→".dimmed());
367 Lockfile::load(None).unwrap_or_else(|_| {
368 println!(
369 "{} No nika.lock found, will use latest versions",
370 "⚠".yellow()
371 );
372 Lockfile::new()
373 })
374 } else {
375 Lockfile::new()
376 };
377
378 let client = RegistryClient::new().map_err(|e| NikaError::ConfigError {
380 reason: format!("Failed to create registry client: {e}"),
381 })?;
382 let mut registry = load_registry()?;
383 let mut installed_count = 0;
384 let mut skipped_count = 0;
385
386 println!();
387 println!(
388 "{} Installing {} dependencies...",
389 "📦".cyan(),
390 manifest.dependencies.len()
391 );
392
393 for (name, version_spec) in &manifest.dependencies {
394 let target_version = if frozen {
396 lockfile
398 .find_version(name)
399 .map_or_else(|| version_spec.clone(), |v| v.to_string())
400 } else {
401 if version_spec == "*" || version_spec == "latest" {
403 match client.get_package(name).await {
404 Ok(info) => info.latest_version.clone(),
405 Err(_) => {
406 println!(" {} {} - not found", "✗".red(), name.cyan());
407 continue;
408 }
409 }
410 } else {
411 version_spec.trim_start_matches('^').to_string()
412 }
413 };
414
415 if is_version_installed(name, &target_version)? {
417 println!(
418 " {} {}@{} (already installed)",
419 "✓".green(),
420 name.cyan(),
421 target_version.dimmed()
422 );
423 skipped_count += 1;
424 continue;
425 }
426
427 let target_dir = package_dir(name, &target_version)?;
429 match client
430 .download_and_extract(name, &target_version, &target_dir)
431 .await
432 {
433 Ok(_) => {
434 let manifest_path =
436 format!("packages/{name}/{target_version}/manifest.yaml");
437 registry.insert(
438 name.clone(),
439 InstalledPackage::now(target_version.clone(), manifest_path),
440 );
441 println!(
442 " {} {}@{}",
443 "✓".green(),
444 name.cyan(),
445 target_version.green()
446 );
447 installed_count += 1;
448 }
449 Err(e) => {
450 println!(
451 " {} {} - {}",
452 "✗".red(),
453 name.cyan(),
454 e.to_string().dimmed()
455 );
456 }
457 }
458 }
459
460 save_registry(®istry)?;
462
463 println!();
464 if installed_count > 0 || skipped_count > 0 {
465 println!(
466 "{} {} package(s) installed, {} already up to date",
467 "✓".green(),
468 installed_count,
469 skipped_count
470 );
471 }
472
473 Ok(())
474 }
475
476 PkgAction::Update { package } => {
477 if let Some(ref pkg) = package {
478 println!("{} Updating package: {}", "🔄".cyan(), pkg.green());
479 } else {
480 println!("{} Updating all packages", "🔄".cyan());
481 }
482
483 println!();
484 println!("{} Package update is not yet implemented", "⚠".yellow());
485 Ok(())
486 }
487
488 PkgAction::Outdated => {
489 println!("{} Checking for outdated packages...", "📋".cyan());
490
491 println!();
492 println!(
493 "{} Outdated package detection is not yet implemented",
494 "⚠".yellow()
495 );
496 Ok(())
497 }
498
499 PkgAction::Search {
500 query,
501 r#type,
502 limit,
503 } => {
504 use nika_engine::registry::RegistryClient;
505
506 let search_query = if let Some(ref t) = r#type {
508 format!("{query} type:{t}")
509 } else {
510 query.clone()
511 };
512
513 println!(
514 "{} Searching registry for '{}'...",
515 "🔍".cyan(),
516 query.green()
517 );
518
519 if let Some(ref t) = r#type {
520 println!(" Type filter: {}", t.dimmed());
521 }
522
523 let client = RegistryClient::new().map_err(|e| NikaError::ConfigError {
525 reason: format!("Failed to create registry client: {e}"),
526 })?;
527
528 let response = client.search(&search_query, 1, limit).await.map_err(|e| {
530 NikaError::ValidationError {
531 reason: format!("Search failed: {e}"),
532 }
533 })?;
534
535 println!();
536 if response.results.is_empty() {
537 println!("{} No packages found matching '{}'", "ℹ".cyan(), query);
538 } else {
539 println!(
540 "{} Found {} package(s):",
541 "📦".cyan(),
542 response.results.len()
543 );
544 println!("{}", "─".repeat(60));
545
546 for result in &response.results {
547 println!(
548 " {} {}",
549 result.name.cyan(),
550 format!("v{}", result.version).green()
551 );
552 if let Some(ref desc) = result.description {
553 println!(" {}", desc.dimmed());
554 }
555 if let Some(ref keywords) = result.keywords {
556 if !keywords.is_empty() {
557 println!(" Keywords: {}", keywords.join(", ").dimmed());
558 }
559 }
560 }
561
562 println!("{}", "─".repeat(60));
563 println!();
564 println!("Install with: nika pkg add <package-name>");
565 }
566 Ok(())
567 }
568 }
569}
570
571fn infer_package_type(package: &str) -> Option<&'static str> {
572 if package.starts_with("@workflows/") || package.starts_with("@nika/") {
573 Some("workflow")
574 } else if package.starts_with("@agents/") {
575 Some("agent")
576 } else if package.starts_with("@skills/") {
577 Some("skill")
578 } else if package.starts_with("@prompts/") {
579 Some("prompt")
580 } else if package.starts_with("@jobs/") {
581 Some("job")
582 } else if package.starts_with("@schemas/") || package.starts_with("@novanet/") {
583 Some("schema")
584 } else {
585 None
586 }
587}