2525#include < fstream>
2626#include < string>
2727#include < vector>
28+ #include < unordered_set>
29+ #include < optional>
2830
2931namespace fs = std::filesystem;
3032using json = nlohmann::json;
@@ -35,6 +37,9 @@ namespace vix::commands
3537
3638 namespace
3739 {
40+ static std::string find_latest_version (const json &entry);
41+ static std::vector<std::string> list_versions (const json &entry);
42+
3843 std::string home_dir ()
3944 {
4045#ifdef _WIN32
@@ -72,7 +77,11 @@ namespace vix::commands
7277 {
7378 std::string ns;
7479 std::string name;
75- std::string version;
80+
81+ std::string requestedVersion;
82+ std::string resolvedVersion;
83+
84+ std::string id () const { return ns + " /" + name; }
7685 };
7786
7887 static std::string to_lower (std::string s)
@@ -101,23 +110,52 @@ namespace vix::commands
101110 return false ;
102111
103112 const auto at = raw.find (' @' );
113+
114+ out.ns = trim_copy (raw.substr (0 , slash));
115+
104116 if (at == std::string::npos)
105- return false ;
117+ {
118+ out.name = trim_copy (raw.substr (slash + 1 ));
119+ out.requestedVersion .clear ();
120+ }
121+ else
122+ {
123+ out.name = trim_copy (raw.substr (slash + 1 , at - (slash + 1 )));
124+ out.requestedVersion = trim_copy (raw.substr (at + 1 ));
125+ }
106126
107- out.ns = raw.substr (0 , slash);
108- out.name = raw.substr (slash + 1 , at - (slash + 1 ));
109- out.version = raw.substr (at + 1 );
127+ out.resolvedVersion .clear ();
110128
111- out.ns = trim_copy (out.ns );
112- out.name = trim_copy (out.name );
113- out.version = trim_copy (out.version );
129+ if (out.ns .empty () || out.name .empty ())
130+ return false ;
114131
115- if (out.ns .empty () || out.name .empty () || out.version .empty ())
132+ // Si @ est présent, la version doit etre non vide
133+ if (at != std::string::npos && out.requestedVersion .empty ())
116134 return false ;
117135
118136 return true ;
119137 }
120138
139+ static int resolve_version_v1 (const json &entry, PkgSpec &spec)
140+ {
141+ if (!spec.requestedVersion .empty ())
142+ {
143+ spec.resolvedVersion = spec.requestedVersion ;
144+ return 0 ;
145+ }
146+
147+ const std::string latest = find_latest_version (entry);
148+ if (latest.empty ())
149+ {
150+ vix::cli::util::err_line (std::cerr,
151+ " no versions available for: " + spec.ns + " /" + spec.name );
152+ return 1 ;
153+ }
154+
155+ spec.resolvedVersion = latest;
156+ return 0 ;
157+ }
158+
121159 static json read_json_file_or_throw (const fs::path &p)
122160 {
123161 std::ifstream in (p);
@@ -174,7 +212,7 @@ namespace vix::commands
174212
175213 json dep;
176214 dep[" id" ] = wantedId;
177- dep[" version" ] = spec.version ;
215+ dep[" version" ] = spec.resolvedVersion ; // IMPORTANT
178216 dep[" repo" ] = repoUrl;
179217 dep[" tag" ] = tag;
180218 dep[" commit" ] = commitSha;
@@ -385,6 +423,140 @@ namespace vix::commands
385423 vix::cli::util::warn_line (std::cerr, " - Ensure the tag exists on origin: git push --tags" );
386424 vix::cli::util::warn_line (std::cerr, " - Or publish a new version with a valid tag/commit (recommended)." );
387425 }
426+
427+ static std::optional<PkgSpec> parse_dep_obj_v1 (const json &d)
428+ {
429+ if (!d.is_object ())
430+ return std::nullopt ;
431+
432+ const std::string id = d.value (" id" , " " );
433+ const std::string ver = d.value (" version" , " " );
434+
435+ const auto slash = id.find (' /' );
436+ if (slash == std::string::npos)
437+ return std::nullopt ;
438+
439+ PkgSpec s;
440+ s.ns = trim_copy (id.substr (0 , slash));
441+ s.name = trim_copy (id.substr (slash + 1 ));
442+
443+ s.requestedVersion = trim_copy (ver); // ici
444+ s.resolvedVersion .clear ();
445+
446+ if (s.ns .empty () || s.name .empty () || s.requestedVersion .empty ())
447+ return std::nullopt ;
448+
449+ return s;
450+ }
451+
452+ static std::vector<PkgSpec> read_vix_json_deps_v1 (const fs::path &repoDir)
453+ {
454+ std::vector<PkgSpec> out;
455+
456+ const fs::path p = repoDir / " vix.json" ;
457+ if (!fs::exists (p))
458+ return out;
459+
460+ json j;
461+ try
462+ {
463+ j = read_json_file_or_throw (p);
464+ }
465+ catch (...)
466+ {
467+ return out;
468+ }
469+
470+ if (!j.contains (" deps" ) || !j[" deps" ].is_array ())
471+ return out;
472+
473+ for (const auto &d : j[" deps" ])
474+ {
475+ auto spec = parse_dep_obj_v1 (d);
476+ if (spec)
477+ out.push_back (*spec);
478+ }
479+
480+ return out;
481+ }
482+
483+ static int ensure_install_one_v1 (
484+ PkgSpec &spec,
485+ std::string &outInstalledDir,
486+ std::string &outRepoUrl,
487+ std::string &outCommit,
488+ std::string &outTag)
489+ {
490+ const fs::path p = entry_path (spec.ns , spec.name );
491+ if (!fs::exists (p))
492+ {
493+ vix::cli::util::err_line (std::cerr, " package not found: " + spec.id ());
494+ return 1 ;
495+ }
496+
497+ const json entry = read_json_file_or_throw (p);
498+
499+ const int vr = resolve_version_v1 (entry, spec);
500+ if (vr != 0 )
501+ return vr;
502+
503+ const std::string repoUrl = entry.at (" repo" ).at (" url" ).get <std::string>();
504+ const json versions = entry.at (" versions" );
505+
506+ if (!versions.contains (spec.resolvedVersion ))
507+ {
508+ vix::cli::util::err_line (std::cerr,
509+ " version not found: " + spec.id () + " @" + spec.resolvedVersion );
510+ return 1 ;
511+ }
512+
513+ const json v = versions.at (spec.resolvedVersion );
514+ const std::string tag = v.at (" tag" ).get <std::string>();
515+ const std::string commit = v.at (" commit" ).get <std::string>();
516+
517+ const std::string idDot = spec.ns + " ." + spec.name ;
518+
519+ std::string outDir;
520+ const int rc = clone_checkout (repoUrl, idDot, commit, outDir);
521+ if (rc != 0 )
522+ return rc;
523+
524+ outInstalledDir = outDir;
525+ outRepoUrl = repoUrl;
526+ outCommit = commit;
527+ outTag = tag;
528+
529+ // lockfile = version exacte résolue + commit
530+ write_lockfile_append (spec, repoUrl, commit, tag);
531+
532+ return 0 ;
533+ }
534+
535+ static int install_transitive_v1 (
536+ PkgSpec root,
537+ std::unordered_set<std::string> &visited)
538+ {
539+ // on installe root (resolve inside ensure_install_one_v1)
540+ std::string dir, repo, commit, tag;
541+ const int rc = ensure_install_one_v1 (root, dir, repo, commit, tag);
542+ if (rc != 0 )
543+ return rc;
544+
545+ const std::string key = root.ns + " /" + root.name + " @" + root.resolvedVersion ;
546+ if (visited.count (key))
547+ return 0 ;
548+ visited.insert (key);
549+
550+ const auto deps = read_vix_json_deps_v1 (fs::path (dir));
551+ for (auto d : deps)
552+ {
553+ const int rc2 = install_transitive_v1 (d, visited);
554+ if (rc2 != 0 )
555+ return rc2;
556+ }
557+
558+ return 0 ;
559+ }
388560 }
389561
390562 int AddCommand::run (const std::vector<std::string> &args)
@@ -401,7 +573,8 @@ namespace vix::commands
401573 if (!parse_pkg_spec (raw, spec))
402574 {
403575 vix::cli::util::err_line (std::cerr, " invalid package spec" );
404- vix::cli::util::warn_line (std::cerr, " Expected: <namespace>/<name>@<version>" );
576+ vix::cli::util::warn_line (std::cerr, " Expected: <namespace>/<name>[@<version>]" );
577+ vix::cli::util::warn_line (std::cerr, " Example: vix add gaspardkirira/tree" );
405578 vix::cli::util::warn_line (std::cerr, " Example: vix add gaspardkirira/tree@0.1.0" );
406579 vix::cli::util::warn_line (std::cerr, std::string (" Try search: vix search " ) + raw);
407580 return 1 ;
@@ -410,7 +583,7 @@ namespace vix::commands
410583 const fs::path p = entry_path (spec.ns , spec.name );
411584 if (!fs::exists (p))
412585 {
413- vix::cli::util::err_line (std::cerr, " package not found: " + spec.ns + " / " + spec. name );
586+ vix::cli::util::err_line (std::cerr, " package not found: " + spec.id () );
414587
415588 vix::cli::util::section (std::cout, " Search" );
416589 vix::cli::util::kv (std::cout, " query" , vix::cli::util::quote (spec.name ));
@@ -426,12 +599,22 @@ namespace vix::commands
426599 {
427600 const json entry = read_json_file_or_throw (p);
428601
429- const std::string repoUrl = entry.at (" repo" ).at (" url" ).get <std::string>();
430- const json versions = entry.at (" versions" );
602+ // resolve version (latest if omitted)
603+ const int vr = resolve_version_v1 (entry, spec);
604+ if (vr != 0 )
605+ return vr;
431606
432- if (!versions. contains ( spec.version ))
607+ if (spec.requestedVersion . empty ( ))
433608 {
434- vix::cli::util::err_line (std::cerr, " version not found: " + spec.version );
609+ vix::cli::util::ok_line (std::cout,
610+ " resolved: " + spec.ns + " /" + spec.name + " @" + spec.resolvedVersion );
611+ }
612+
613+ const json versions = entry.at (" versions" );
614+ if (!versions.contains (spec.resolvedVersion ))
615+ {
616+ vix::cli::util::err_line (std::cerr,
617+ " version not found: " + spec.id () + " @" + spec.resolvedVersion );
435618
436619 const std::string latest = find_latest_version (entry);
437620 const auto all = list_versions (entry);
@@ -445,49 +628,34 @@ namespace vix::commands
445628 }
446629
447630 if (!latest.empty ())
448- vix::cli::util::warn_line (std::cerr, " Try: vix add " + spec.ns + " / " + spec. name + " @" + latest);
631+ vix::cli::util::warn_line (std::cerr, " Try: vix add " + spec.id () + " @" + latest);
449632
450633 return 1 ;
451634 }
452635
453- const json v = versions.at (spec.version );
636+ const json v = versions.at (spec.resolvedVersion );
454637 const std::string tag = v.at (" tag" ).get <std::string>();
455638 const std::string commit = v.at (" commit" ).get <std::string>();
456639
457- const std::string pkgId = spec. ns + " / " + spec. name ;
458- const std::string idDot = spec.ns + " . " + spec. name ;
640+ const std::string repoUrl = entry. at ( " repo " ). at ( " url " ). get <std::string>() ;
641+ const std::string pkgId = spec.id () ;
459642
460643 vix::cli::util::section (std::cout, " Add" );
461644 vix::cli::util::kv (std::cout, " id" , pkgId);
462- vix::cli::util::kv (std::cout, " version" , spec.version );
645+ vix::cli::util::kv (std::cout, " version" , spec.resolvedVersion );
463646 vix::cli::util::kv (std::cout, " tag" , tag);
464647 vix::cli::util::kv (std::cout, " commit" , commit);
465648
466- step (" fetching sources ..." );
649+ step (" installing dependencies (transitive) ..." );
467650
468- std::string outDir ;
469- const int rc = clone_checkout (repoUrl, idDot, commit, outDir );
651+ std::unordered_set<std:: string> visited ;
652+ const int rc = install_transitive_v1 (spec, visited );
470653 if (rc != 0 )
471- {
472- vix::cli::util::err_line (std::cerr, " fetch failed" );
473-
474- print_commit_missing_help (pkgId, repoUrl, tag, commit);
475-
476- vix::cli::util::section (std::cout, " Search" );
477- vix::cli::util::kv (std::cout, " query" , vix::cli::util::quote (spec.name ));
478- const auto hits = search_registry_local (spec.name );
479- print_search_hits (hits);
480-
481- vix::cli::util::warn_line (std::cerr, " Then retry: vix add " + pkgId + " @" + spec.version );
482654 return rc;
483- }
484655
485- write_lockfile_append (spec, repoUrl, commit, tag);
486-
487- vix::cli::util::ok_line (std::cout, " added: " + pkgId + " (pinned " + commit + " )" );
656+ vix::cli::util::ok_line (std::cout, " added: " + pkgId + " @" + spec.resolvedVersion );
488657 vix::cli::util::ok_line (std::cout, " lock: " + lock_path ().string ());
489- vix::cli::util::ok_line (std::cout, " store: " + outDir);
490-
658+ vix::cli::util::ok_line (std::cout, " deps: " + std::to_string (visited.size ()));
491659 return 0 ;
492660 }
493661 catch (const std::exception &ex)
@@ -501,13 +669,27 @@ namespace vix::commands
501669 {
502670 std::cout
503671 << " Usage:\n "
504- << " vix add <namespace>/<name>@<version>\n\n "
505- << " Example:\n "
672+ << " vix add <namespace>/<name>[@<version>]\n\n "
673+
674+ << " Description:\n "
675+ << " Install a package from the Vix Registry.\n "
676+ << " If @version is omitted, the latest version is resolved automatically.\n\n "
677+
678+ << " Examples:\n "
506679 << " vix registry sync\n "
680+ << " vix add gaspardkirira/tree\n "
507681 << " vix add gaspardkirira/tree@0.1.0\n\n "
682+
683+ << " Behavior:\n "
684+ << " - Resolves the requested version (or latest if omitted)\n "
685+ << " - Clones the repository at the exact commit\n "
686+ << " - Installs transitive dependencies\n "
687+ << " - Writes a vix.lock file pinning the resolved commit SHA\n\n "
688+
508689 << " Notes:\n "
509- << " - V1 requires an exact version.\n "
510- << " - The lockfile pins the resolved commit SHA.\n " ;
690+ << " - Run 'vix registry sync' if a package cannot be found.\n "
691+ << " - The lockfile guarantees deterministic builds.\n " ;
692+
511693 return 0 ;
512694 }
513695}
0 commit comments