Skip to content

Commit e5704ca

Browse files
committed
cli(add): support optional @Version and proper transitive dependency resolution
- Allow 'vix add <ns>/<name>' without explicit version - Resolve latest version automatically when omitted - Fix lockfile to pin resolvedVersion correctly - Improve transitive dependency installation logic - Clean up internal helpers and ambiguity issues
1 parent 133cdd0 commit e5704ca

1 file changed

Lines changed: 227 additions & 45 deletions

File tree

src/commands/AddCommand.cpp

Lines changed: 227 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include <fstream>
2626
#include <string>
2727
#include <vector>
28+
#include <unordered_set>
29+
#include <optional>
2830

2931
namespace fs = std::filesystem;
3032
using 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

Comments
 (0)