Skip to content

Commit e2a8728

Browse files
committed
feat(cli): introduce semver resolver and fix version handling across commands
- add Semver helper with proper version comparison and range resolution - replace lexicographic version comparisons with semantic comparison - fix latest version detection in search and outdated commands - fix install and add commands to resolve correct versions - prepare CLI for semver ranges support (future manifest integration) This fixes incorrect ordering like 1.9.0 > 1.10.0 and ensures consistent version resolution across Vix.
1 parent 904059d commit e2a8728

File tree

8 files changed

+967
-58
lines changed

8 files changed

+967
-58
lines changed

include/vix/cli/util/Semver.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
*
3+
* @file Semver.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_CLI_UTIL_SEMVER_HPP
14+
#define VIX_CLI_UTIL_SEMVER_HPP
15+
16+
#include <optional>
17+
#include <string>
18+
#include <vector>
19+
20+
namespace vix::cli::util::semver
21+
{
22+
int compare(const std::string &lhs, const std::string &rhs);
23+
bool satisfies(const std::string &version, const std::string &range);
24+
std::optional<std::string> resolveMaxSatisfying(
25+
const std::vector<std::string> &versions,
26+
const std::string &range);
27+
std::string findLatest(const std::vector<std::string> &versions);
28+
void sortAscending(std::vector<std::string> &versions);
29+
void sortDescending(std::vector<std::string> &versions);
30+
}
31+
32+
#endif

include/vix/cli/util/Version.hpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
*
3+
* @file Version.hpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*/
13+
#ifndef VIX_CLI_UTIL_VERSION_HPP
14+
#define VIX_CLI_UTIL_VERSION_HPP
15+
16+
#include <string>
17+
#include <vector>
18+
19+
namespace vix::cli::util
20+
{
21+
std::vector<int> parseVersionParts(const std::string &version);
22+
int compareVersions(const std::string &lhs, const std::string &rhs);
23+
bool isVersionGreater(const std::string &lhs, const std::string &rhs);
24+
std::string findLatestVersionFromJsonObjectKeys(const std::vector<std::string> &versions);
25+
}
26+
27+
#endif

src/commands/AddCommand.cpp

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <vix/cli/util/Shell.hpp>
1616
#include <vix/cli/util/Ui.hpp>
1717
#include <vix/cli/util/Hash.hpp>
18+
#include <vix/cli/util/Semver.hpp>
1819
#include <vix/cli/Style.hpp>
1920
#include <vix/utils/Env.hpp>
2021
#include <nlohmann/json.hpp>
@@ -149,21 +150,43 @@ namespace vix::commands
149150

150151
static int resolve_version_v1(const json &entry, PkgSpec &spec)
151152
{
152-
if (!spec.requestedVersion.empty())
153+
if (!entry.contains("versions") || !entry["versions"].is_object())
153154
{
154-
spec.resolvedVersion = spec.requestedVersion;
155+
vix::cli::util::err_line(std::cerr,
156+
"invalid registry entry: missing versions for " + spec.id());
157+
return 1;
158+
}
159+
160+
std::vector<std::string> versions;
161+
versions.reserve(entry["versions"].size());
162+
163+
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
164+
versions.push_back(it.key());
165+
166+
if (versions.empty())
167+
{
168+
vix::cli::util::err_line(std::cerr,
169+
"no versions available for: " + spec.id());
170+
return 1;
171+
}
172+
173+
if (spec.requestedVersion.empty())
174+
{
175+
spec.resolvedVersion = vix::cli::util::semver::findLatest(versions);
155176
return 0;
156177
}
157178

158-
const std::string latest = find_latest_version(entry);
159-
if (latest.empty())
179+
const auto resolved =
180+
vix::cli::util::semver::resolveMaxSatisfying(versions, spec.requestedVersion);
181+
182+
if (!resolved.has_value())
160183
{
161184
vix::cli::util::err_line(std::cerr,
162-
"no versions available for: " + spec.ns + "/" + spec.name);
185+
"no version matches range: " + spec.id() + "@" + spec.requestedVersion);
163186
return 1;
164187
}
165188

166-
spec.resolvedVersion = latest;
189+
spec.resolvedVersion = *resolved;
167190
return 0;
168191
}
169192

@@ -355,18 +378,16 @@ namespace vix::commands
355378
if (entry.contains("latest") && entry["latest"].is_string())
356379
return entry["latest"].get<std::string>();
357380

358-
if (entry.contains("versions") && entry["versions"].is_object())
359-
{
360-
std::string best;
361-
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
362-
{
363-
const std::string v = it.key();
364-
if (best.empty() || v > best)
365-
best = v;
366-
}
367-
return best;
368-
}
369-
return {};
381+
if (!entry.contains("versions") || !entry["versions"].is_object())
382+
return {};
383+
384+
std::vector<std::string> versions;
385+
versions.reserve(entry["versions"].size());
386+
387+
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
388+
versions.push_back(it.key());
389+
390+
return vix::cli::util::semver::findLatest(versions);
370391
}
371392

372393
static std::vector<std::string> list_versions(const json &entry)
@@ -378,7 +399,7 @@ namespace vix::commands
378399
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
379400
out.push_back(it.key());
380401

381-
std::sort(out.begin(), out.end());
402+
vix::cli::util::semver::sortAscending(out);
382403
return out;
383404
}
384405

src/commands/InstallCommand.cpp

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <vix/cli/util/Hash.hpp>
1818
#include <vix/cli/Style.hpp>
1919
#include <vix/utils/Env.hpp>
20+
#include <vix/cli/util/Semver.hpp>
2021

2122
#include <nlohmann/json.hpp>
2223

@@ -276,39 +277,62 @@ namespace vix::commands
276277
if (entry.contains("latest") && entry["latest"].is_string())
277278
return entry["latest"].get<std::string>();
278279

279-
if (entry.contains("versions") && entry["versions"].is_object())
280-
{
281-
std::string best;
282-
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
283-
{
284-
const std::string v = it.key();
285-
if (best.empty() || v > best)
286-
best = v;
287-
}
288-
return best;
289-
}
280+
if (!entry.contains("versions") || !entry["versions"].is_object())
281+
return {};
282+
283+
std::vector<std::string> versions;
284+
versions.reserve(entry["versions"].size());
290285

291-
return {};
286+
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
287+
versions.push_back(it.key());
288+
289+
return vix::cli::util::semver::findLatest(versions);
292290
}
293291

294292
static int resolve_version_v1(const json &entry, PkgSpec &spec)
295293
{
296-
if (!spec.requestedVersion.empty())
294+
if (!entry.contains("versions") || !entry["versions"].is_object())
297295
{
298-
spec.resolvedVersion = spec.requestedVersion;
296+
vix::cli::util::err_line(
297+
std::cerr,
298+
"invalid registry entry: missing versions for " + spec.id());
299+
return 1;
300+
}
301+
302+
std::vector<std::string> versions;
303+
versions.reserve(entry["versions"].size());
304+
305+
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
306+
versions.push_back(it.key());
307+
308+
if (versions.empty())
309+
{
310+
vix::cli::util::err_line(
311+
std::cerr,
312+
"no versions available for: " + spec.id());
313+
return 1;
314+
}
315+
316+
if (spec.requestedVersion.empty())
317+
{
318+
spec.resolvedVersion = vix::cli::util::semver::findLatest(versions);
299319
return 0;
300320
}
301321

302-
const std::string latest = find_latest_version(entry);
303-
if (latest.empty())
322+
const auto resolved =
323+
vix::cli::util::semver::resolveMaxSatisfying(
324+
versions,
325+
spec.requestedVersion);
326+
327+
if (!resolved.has_value())
304328
{
305329
vix::cli::util::err_line(
306330
std::cerr,
307-
"no versions available for: " + spec.ns + "/" + spec.name);
331+
"no version matches range: " + spec.id() + "@" + spec.requestedVersion);
308332
return 1;
309333
}
310334

311-
spec.resolvedVersion = latest;
335+
spec.resolvedVersion = *resolved;
312336
return 0;
313337
}
314338

src/commands/OutdatedCommand.cpp

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
#include <vix/cli/commands/OutdatedCommand.hpp>
1515
#include <vix/cli/util/Ui.hpp>
16+
#include <vix/cli/util/Semver.hpp>
1617
#include <vix/utils/Env.hpp>
1718
#include <nlohmann/json.hpp>
1819

@@ -248,28 +249,27 @@ namespace vix::commands
248249
return registry_index_dir() / (ns + "." + name + ".json");
249250
}
250251

251-
std::string find_latest_version(const json &entry)
252+
static std::string find_latest_version(const json &entry)
252253
{
253254
if (entry.contains("latest") && entry["latest"].is_string())
254255
{
255256
return entry["latest"].get<std::string>();
256257
}
257258

258-
if (entry.contains("versions") && entry["versions"].is_object())
259+
if (!entry.contains("versions") || !entry["versions"].is_object())
259260
{
260-
std::string best;
261-
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
262-
{
263-
const std::string v = it.key();
264-
if (best.empty() || v > best)
265-
{
266-
best = v;
267-
}
268-
}
269-
return best;
261+
return {};
270262
}
271263

272-
return {};
264+
std::vector<std::string> versions;
265+
versions.reserve(entry["versions"].size());
266+
267+
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
268+
{
269+
versions.push_back(it.key());
270+
}
271+
272+
return vix::cli::util::semver::findLatest(versions);
273273
}
274274

275275
bool lock_contains_dependency_id(const json &lock, const std::string &wantedId)

src/commands/SearchCommand.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <vix/cli/commands/SearchCommand.hpp>
1515
#include <vix/cli/util/Ui.hpp>
1616
#include <vix/cli/Style.hpp>
17+
#include <vix/cli/util/Semver.hpp>
1718
#include <vix/utils/Env.hpp>
1819
#include <nlohmann/json.hpp>
1920

@@ -109,22 +110,21 @@ namespace vix::commands
109110
return out;
110111
}
111112

112-
std::string latest_version(const json &entry)
113+
static std::string latest_version(const json &entry)
113114
{
114115
if (entry.contains("latest") && entry["latest"].is_string())
115116
return entry["latest"].get<std::string>();
116117

117118
if (!entry.contains("versions") || !entry["versions"].is_object())
118119
return {};
119120

120-
std::string best;
121+
std::vector<std::string> versions;
122+
versions.reserve(entry["versions"].size());
123+
121124
for (auto it = entry["versions"].begin(); it != entry["versions"].end(); ++it)
122-
{
123-
const std::string v = it.key();
124-
if (best.empty() || v > best)
125-
best = v;
126-
}
127-
return best;
125+
versions.push_back(it.key());
126+
127+
return vix::cli::util::semver::findLatest(versions);
128128
}
129129

130130
struct Hit

0 commit comments

Comments
 (0)