@ebowwa/pkg-ops 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -16
- package/dist/index.js.map +5 -7
- package/package.json +2 -2
- package/rust/src/lib.rs +438 -0
- package/rust/src/main.rs +183 -0
- package/src/index.ts +4 -2
package/rust/src/lib.rs
CHANGED
|
@@ -94,6 +94,52 @@ pub struct InstalledPackageInfo {
|
|
|
94
94
|
pub version: String,
|
|
95
95
|
pub dist_size_bytes: Option<u64>,
|
|
96
96
|
pub installed_at: Option<String>, // ISO timestamp
|
|
97
|
+
pub total_versions: Option<u32>,
|
|
98
|
+
pub versions: Option<Vec<VersionInfo>>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Version information for multi-version support
|
|
102
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
103
|
+
#[serde(rename_all = "camelCase")]
|
|
104
|
+
pub struct VersionInfo {
|
|
105
|
+
pub version: String,
|
|
106
|
+
pub installed_at: String,
|
|
107
|
+
pub dist_size_bytes: Option<u64>,
|
|
108
|
+
pub file_count: Option<u32>,
|
|
109
|
+
pub active: bool,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Result of switching versions
|
|
113
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
114
|
+
#[serde(rename_all = "camelCase")]
|
|
115
|
+
pub struct SwitchResult {
|
|
116
|
+
pub success: bool,
|
|
117
|
+
pub package_name: String,
|
|
118
|
+
pub from_version: String,
|
|
119
|
+
pub to_version: String,
|
|
120
|
+
pub message: String,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Result of pruning old versions
|
|
124
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
125
|
+
#[serde(rename_all = "camelCase")]
|
|
126
|
+
pub struct PruneResult {
|
|
127
|
+
pub success: bool,
|
|
128
|
+
pub package_name: String,
|
|
129
|
+
pub removed_versions: Vec<String>,
|
|
130
|
+
pub kept_versions: Vec<String>,
|
|
131
|
+
pub freed_bytes: u64,
|
|
132
|
+
pub message: String,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Multi-version package summary
|
|
136
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
137
|
+
#[serde(rename_all = "camelCase")]
|
|
138
|
+
pub struct MultiVersionPackage {
|
|
139
|
+
pub package_name: String,
|
|
140
|
+
pub active_version: String,
|
|
141
|
+
pub total_versions: u32,
|
|
142
|
+
pub versions: Vec<String>,
|
|
97
143
|
}
|
|
98
144
|
|
|
99
145
|
// ---------------------------------------------------------------------------
|
|
@@ -797,3 +843,395 @@ pub fn get_installed_info() -> Vec<InstalledPackageInfo> {
|
|
|
797
843
|
|
|
798
844
|
info_list
|
|
799
845
|
}
|
|
846
|
+
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
// Multi-Version Operations
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
|
|
851
|
+
/// Directory for storing multiple package versions
|
|
852
|
+
const VERSIONS_DIR: &str = "/opt/pkg-ops/versions";
|
|
853
|
+
|
|
854
|
+
/// List all installed versions of a package
|
|
855
|
+
pub fn list_versions(package_name: &str) -> Vec<VersionInfo> {
|
|
856
|
+
info!("Listing versions for {}", package_name);
|
|
857
|
+
|
|
858
|
+
let mut versions = Vec::new();
|
|
859
|
+
|
|
860
|
+
// Check the multi-version directory
|
|
861
|
+
let package_versions_dir = Path::new(VERSIONS_DIR)
|
|
862
|
+
.join(package_name.replace("@", "").replace("/", "-"));
|
|
863
|
+
|
|
864
|
+
if package_versions_dir.exists() {
|
|
865
|
+
if let Ok(entries) = fs::read_dir(&package_versions_dir) {
|
|
866
|
+
for entry in entries.flatten() {
|
|
867
|
+
let version_dir = entry.path();
|
|
868
|
+
if !version_dir.is_dir() {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
let version = entry.file_name().to_string_lossy().to_string();
|
|
873
|
+
let dist_path = version_dir.join("dist");
|
|
874
|
+
|
|
875
|
+
// Get installed timestamp from directory mtime
|
|
876
|
+
let installed_at = fs::metadata(&version_dir)
|
|
877
|
+
.ok()
|
|
878
|
+
.and_then(|m| m.modified().ok())
|
|
879
|
+
.map(|t| {
|
|
880
|
+
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
|
881
|
+
datetime.to_rfc3339()
|
|
882
|
+
})
|
|
883
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
884
|
+
|
|
885
|
+
let (dist_size, file_count) = if dist_path.exists() {
|
|
886
|
+
let (size, count) = calculate_dir_size(&dist_path);
|
|
887
|
+
(Some(size), Some(count))
|
|
888
|
+
} else {
|
|
889
|
+
(None, None)
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
versions.push(VersionInfo {
|
|
893
|
+
version,
|
|
894
|
+
installed_at,
|
|
895
|
+
dist_size_bytes: dist_size,
|
|
896
|
+
file_count,
|
|
897
|
+
active: false, // Will be set below
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Also check the active version in node_modules
|
|
904
|
+
let active_version = get_installed_version(package_name);
|
|
905
|
+
if let Some(ref active) = active_version {
|
|
906
|
+
// Mark active version
|
|
907
|
+
for v in &mut versions {
|
|
908
|
+
if &v.version == active {
|
|
909
|
+
v.active = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// If active version not in versions list, add it
|
|
914
|
+
if !versions.iter().any(|v| &v.version == active) {
|
|
915
|
+
let package_path = Path::new(WORK_DIR)
|
|
916
|
+
.join("node_modules")
|
|
917
|
+
.join(package_name);
|
|
918
|
+
|
|
919
|
+
let dist_path = package_path.join("dist");
|
|
920
|
+
let (dist_size, file_count) = if dist_path.exists() {
|
|
921
|
+
let (size, count) = calculate_dir_size(&dist_path);
|
|
922
|
+
(Some(size), Some(count))
|
|
923
|
+
} else {
|
|
924
|
+
(None, None)
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
let installed_at = package_path.join("package.json")
|
|
928
|
+
.exists()
|
|
929
|
+
.then(|| {
|
|
930
|
+
fs::metadata(package_path.join("package.json"))
|
|
931
|
+
.ok()
|
|
932
|
+
.and_then(|m| m.modified().ok())
|
|
933
|
+
.map(|t| {
|
|
934
|
+
let datetime: chrono::DateTime<chrono::Utc> = t.into();
|
|
935
|
+
datetime.to_rfc3339()
|
|
936
|
+
})
|
|
937
|
+
})
|
|
938
|
+
.flatten()
|
|
939
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
940
|
+
|
|
941
|
+
versions.push(VersionInfo {
|
|
942
|
+
version: active.clone(),
|
|
943
|
+
installed_at,
|
|
944
|
+
dist_size_bytes: dist_size,
|
|
945
|
+
file_count,
|
|
946
|
+
active: true,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Sort by version descending (newest first)
|
|
952
|
+
versions.sort_by(|a, b| {
|
|
953
|
+
// Simple semver comparison
|
|
954
|
+
let a_parts: Vec<u32> = a.version
|
|
955
|
+
.split('.')
|
|
956
|
+
.filter_map(|s| s.parse().ok())
|
|
957
|
+
.collect();
|
|
958
|
+
let b_parts: Vec<u32> = b.version
|
|
959
|
+
.split('.')
|
|
960
|
+
.filter_map(|s| s.parse().ok())
|
|
961
|
+
.collect();
|
|
962
|
+
|
|
963
|
+
for i in 0..std::cmp::min(a_parts.len(), b_parts.len()) {
|
|
964
|
+
match b_parts.get(i).cmp(&a_parts.get(i)) {
|
|
965
|
+
std::cmp::Ordering::Equal => continue,
|
|
966
|
+
other => return other,
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
b_parts.len().cmp(&a_parts.len())
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
versions
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/// Switch to a specific installed version
|
|
976
|
+
pub fn switch_version(package_name: &str, target_version: &str) -> SwitchResult {
|
|
977
|
+
info!("Switching {} to version {}", package_name, target_version);
|
|
978
|
+
|
|
979
|
+
let current_version = match get_installed_version(package_name) {
|
|
980
|
+
Some(v) => v,
|
|
981
|
+
None => {
|
|
982
|
+
return SwitchResult {
|
|
983
|
+
success: false,
|
|
984
|
+
package_name: package_name.to_string(),
|
|
985
|
+
from_version: "none".to_string(),
|
|
986
|
+
to_version: target_version.to_string(),
|
|
987
|
+
message: "Package not installed".to_string(),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
if current_version == target_version {
|
|
993
|
+
return SwitchResult {
|
|
994
|
+
success: true,
|
|
995
|
+
package_name: package_name.to_string(),
|
|
996
|
+
from_version: current_version.clone(),
|
|
997
|
+
to_version: target_version.to_string(),
|
|
998
|
+
message: "Already on target version".to_string(),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Check if target version exists in versions directory
|
|
1003
|
+
let version_dir = Path::new(VERSIONS_DIR)
|
|
1004
|
+
.join(package_name.replace("@", "").replace("/", "-"))
|
|
1005
|
+
.join(target_version);
|
|
1006
|
+
|
|
1007
|
+
if !version_dir.exists() {
|
|
1008
|
+
// Try installing from npm
|
|
1009
|
+
let package_spec = format!("{}@{}", package_name, target_version);
|
|
1010
|
+
let output = match Command::new("bun")
|
|
1011
|
+
.args(["add", &package_spec])
|
|
1012
|
+
.current_dir(WORK_DIR)
|
|
1013
|
+
.output()
|
|
1014
|
+
{
|
|
1015
|
+
Ok(o) => o,
|
|
1016
|
+
Err(e) => {
|
|
1017
|
+
return SwitchResult {
|
|
1018
|
+
success: false,
|
|
1019
|
+
package_name: package_name.to_string(),
|
|
1020
|
+
from_version: current_version,
|
|
1021
|
+
to_version: target_version.to_string(),
|
|
1022
|
+
message: format!("Failed to run bun add: {}", e),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
if !output.status.success() {
|
|
1028
|
+
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1029
|
+
return SwitchResult {
|
|
1030
|
+
success: false,
|
|
1031
|
+
package_name: package_name.to_string(),
|
|
1032
|
+
from_version: current_version,
|
|
1033
|
+
to_version: target_version.to_string(),
|
|
1034
|
+
message: format!("Version {} not found locally or on npm: {}", target_version, stderr),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return SwitchResult {
|
|
1039
|
+
success: true,
|
|
1040
|
+
package_name: package_name.to_string(),
|
|
1041
|
+
from_version: current_version,
|
|
1042
|
+
to_version: target_version.to_string(),
|
|
1043
|
+
message: format!("Installed and switched to {}", target_version),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Backup current version to versions directory
|
|
1048
|
+
let current_version_dir = Path::new(VERSIONS_DIR)
|
|
1049
|
+
.join(package_name.replace("@", "").replace("/", "-"))
|
|
1050
|
+
.join(¤t_version);
|
|
1051
|
+
|
|
1052
|
+
let current_package_path = Path::new(WORK_DIR)
|
|
1053
|
+
.join("node_modules")
|
|
1054
|
+
.join(package_name);
|
|
1055
|
+
|
|
1056
|
+
if current_package_path.exists() {
|
|
1057
|
+
// Create versions directory if needed
|
|
1058
|
+
let _ = fs::create_dir_all(current_version_dir.parent().unwrap());
|
|
1059
|
+
|
|
1060
|
+
// Copy current version to versions dir (if not already there)
|
|
1061
|
+
if !current_version_dir.exists() {
|
|
1062
|
+
if let Err(e) = copy_dir_all(¤t_package_path, ¤t_version_dir) {
|
|
1063
|
+
error!("Failed to backup current version: {}", e);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Remove current version from node_modules
|
|
1069
|
+
if let Err(e) = fs::remove_dir_all(¤t_package_path) {
|
|
1070
|
+
error!("Failed to remove current version: {}", e);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Copy target version to node_modules
|
|
1074
|
+
if let Err(e) = copy_dir_all(&version_dir, ¤t_package_path) {
|
|
1075
|
+
return SwitchResult {
|
|
1076
|
+
success: false,
|
|
1077
|
+
package_name: package_name.to_string(),
|
|
1078
|
+
from_version: current_version,
|
|
1079
|
+
to_version: target_version.to_string(),
|
|
1080
|
+
message: format!("Failed to copy target version: {}", e),
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
info!("Switched {} from {} to {}", package_name, current_version, target_version);
|
|
1085
|
+
|
|
1086
|
+
SwitchResult {
|
|
1087
|
+
success: true,
|
|
1088
|
+
package_name: package_name.to_string(),
|
|
1089
|
+
from_version: current_version,
|
|
1090
|
+
to_version: target_version.to_string(),
|
|
1091
|
+
message: format!("Switched from {} to {}", current_version, target_version),
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/// Prune old versions, keeping only N most recent
|
|
1096
|
+
pub fn prune_versions(package_name: &str, keep_count: u32) -> PruneResult {
|
|
1097
|
+
info!("Pruning {} versions, keeping {}", package_name, keep_count);
|
|
1098
|
+
|
|
1099
|
+
let versions = list_versions(package_name);
|
|
1100
|
+
|
|
1101
|
+
if versions.len() <= keep_count as usize {
|
|
1102
|
+
return PruneResult {
|
|
1103
|
+
success: true,
|
|
1104
|
+
package_name: package_name.to_string(),
|
|
1105
|
+
removed_versions: vec![],
|
|
1106
|
+
kept_versions: versions.iter().map(|v| v.version.clone()).collect(),
|
|
1107
|
+
freed_bytes: 0,
|
|
1108
|
+
message: format!("Only {} versions installed, nothing to prune", versions.len()),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
let package_versions_dir = Path::new(VERSIONS_DIR)
|
|
1113
|
+
.join(package_name.replace("@", "").replace("/", "-"));
|
|
1114
|
+
|
|
1115
|
+
// Sort by installed date (newest first) and keep top N
|
|
1116
|
+
let mut sorted_versions: Vec<_> = versions.into_iter().collect();
|
|
1117
|
+
sorted_versions.sort_by(|a, b| b.installed_at.cmp(&a.installed_at));
|
|
1118
|
+
|
|
1119
|
+
let kept: Vec<_> = sorted_versions.iter()
|
|
1120
|
+
.take(keep_count as usize)
|
|
1121
|
+
.map(|v| v.version.clone())
|
|
1122
|
+
.collect();
|
|
1123
|
+
|
|
1124
|
+
let mut removed = Vec::new();
|
|
1125
|
+
let mut freed_bytes = 0u64;
|
|
1126
|
+
|
|
1127
|
+
for version_info in sorted_versions.iter().skip(keep_count as usize) {
|
|
1128
|
+
// Don't remove active version
|
|
1129
|
+
if version_info.active {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
let version_dir = package_versions_dir.join(&version_info.version);
|
|
1134
|
+
if version_dir.exists() {
|
|
1135
|
+
// Calculate size before removing
|
|
1136
|
+
let (size, _) = calculate_dir_size(&version_dir);
|
|
1137
|
+
freed_bytes += size;
|
|
1138
|
+
|
|
1139
|
+
if let Err(e) = fs::remove_dir_all(&version_dir) {
|
|
1140
|
+
error!("Failed to remove version {}: {}", version_info.version, e);
|
|
1141
|
+
} else {
|
|
1142
|
+
removed.push(version_info.version.clone());
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
PruneResult {
|
|
1148
|
+
success: true,
|
|
1149
|
+
package_name: package_name.to_string(),
|
|
1150
|
+
removed_versions: removed.clone(),
|
|
1151
|
+
kept_versions: kept,
|
|
1152
|
+
freed_bytes,
|
|
1153
|
+
message: if removed.is_empty() {
|
|
1154
|
+
"No versions to remove".to_string()
|
|
1155
|
+
} else {
|
|
1156
|
+
format!("Removed {} version(s), freed {} bytes", removed.len(), freed_bytes)
|
|
1157
|
+
},
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/// Remove a specific version
|
|
1162
|
+
pub fn remove_version(package_name: &str, version: &str) -> Result<String, String> {
|
|
1163
|
+
info!("Removing {}@{}", package_name, version);
|
|
1164
|
+
|
|
1165
|
+
// Check if this is the active version
|
|
1166
|
+
let active_version = get_installed_version(package_name);
|
|
1167
|
+
if active_version.as_deref() == Some(version) {
|
|
1168
|
+
return Err("Cannot remove active version. Switch to another version first.".to_string());
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
let version_dir = Path::new(VERSIONS_DIR)
|
|
1172
|
+
.join(package_name.replace("@", "").replace("/", "-"))
|
|
1173
|
+
.join(version);
|
|
1174
|
+
|
|
1175
|
+
if !version_dir.exists() {
|
|
1176
|
+
return Err(format!("Version {} not found", version));
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
let (size, _) = calculate_dir_size(&version_dir);
|
|
1180
|
+
|
|
1181
|
+
if let Err(e) = fs::remove_dir_all(&version_dir) {
|
|
1182
|
+
return Err(format!("Failed to remove version: {}", e));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
Ok(format!("Removed {}@{} (freed {} bytes)", package_name, version, size))
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/// Get packages with multiple versions installed
|
|
1189
|
+
pub fn get_multi_version_packages() -> Vec<MultiVersionPackage> {
|
|
1190
|
+
info!("Getting packages with multiple versions");
|
|
1191
|
+
|
|
1192
|
+
let packages = list_packages();
|
|
1193
|
+
let mut result = Vec::new();
|
|
1194
|
+
|
|
1195
|
+
for pkg in packages {
|
|
1196
|
+
if !pkg.installed {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
let versions = list_versions(&pkg.name);
|
|
1201
|
+
if versions.len() > 1 {
|
|
1202
|
+
let active_version = versions.iter()
|
|
1203
|
+
.find(|v| v.active)
|
|
1204
|
+
.map(|v| v.version.clone())
|
|
1205
|
+
.unwrap_or_else(|| pkg.version.clone());
|
|
1206
|
+
|
|
1207
|
+
result.push(MultiVersionPackage {
|
|
1208
|
+
package_name: pkg.name.clone(),
|
|
1209
|
+
active_version,
|
|
1210
|
+
total_versions: versions.len() as u32,
|
|
1211
|
+
versions: versions.iter().map(|v| v.version.clone()).collect(),
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
result
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/// Helper to recursively copy a directory
|
|
1220
|
+
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
|
|
1221
|
+
fs::create_dir_all(dst)?;
|
|
1222
|
+
|
|
1223
|
+
for entry in fs::read_dir(src)? {
|
|
1224
|
+
let entry = entry?;
|
|
1225
|
+
let ty = entry.file_type()?;
|
|
1226
|
+
let src_path = entry.path();
|
|
1227
|
+
let dst_path = dst.join(entry.file_name());
|
|
1228
|
+
|
|
1229
|
+
if ty.is_dir() {
|
|
1230
|
+
copy_dir_all(&src_path, &dst_path)?;
|
|
1231
|
+
} else {
|
|
1232
|
+
fs::copy(&src_path, &dst_path)?;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
Ok(())
|
|
1237
|
+
}
|
package/rust/src/main.rs
CHANGED
|
@@ -227,6 +227,183 @@ fn handle_installed_info(req: Request) -> ResponseEnvelope {
|
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Multi-Version Handlers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
fn handle_list_versions(req: Request) -> ResponseEnvelope {
|
|
235
|
+
let params = match req.params.as_ref() {
|
|
236
|
+
Some(p) => p,
|
|
237
|
+
None => {
|
|
238
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
239
|
+
req.id,
|
|
240
|
+
ErrorObject::invalid_params("Missing params"),
|
|
241
|
+
));
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
let package_name = match params["packageName"].as_str() {
|
|
246
|
+
Some(n) => n,
|
|
247
|
+
None => {
|
|
248
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
249
|
+
req.id,
|
|
250
|
+
ErrorObject::invalid_params("Missing packageName"),
|
|
251
|
+
));
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
let versions = list_versions(package_name);
|
|
256
|
+
|
|
257
|
+
match Response::new(req.id.clone(), &versions) {
|
|
258
|
+
Ok(r) => ResponseEnvelope::Success(r),
|
|
259
|
+
Err(e) => ResponseEnvelope::Error(ErrorResponse::new(
|
|
260
|
+
req.id,
|
|
261
|
+
ErrorObject::internal_error(e.to_string()),
|
|
262
|
+
)),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn handle_switch_version(req: Request) -> ResponseEnvelope {
|
|
267
|
+
let params = match req.params.as_ref() {
|
|
268
|
+
Some(p) => p,
|
|
269
|
+
None => {
|
|
270
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
271
|
+
req.id,
|
|
272
|
+
ErrorObject::invalid_params("Missing params"),
|
|
273
|
+
));
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
let package_name = match params["packageName"].as_str() {
|
|
278
|
+
Some(n) => n,
|
|
279
|
+
None => {
|
|
280
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
281
|
+
req.id,
|
|
282
|
+
ErrorObject::invalid_params("Missing packageName"),
|
|
283
|
+
));
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
let version = match params["version"].as_str() {
|
|
288
|
+
Some(v) => v,
|
|
289
|
+
None => {
|
|
290
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
291
|
+
req.id,
|
|
292
|
+
ErrorObject::invalid_params("Missing version"),
|
|
293
|
+
));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
let result = switch_version(package_name, version);
|
|
298
|
+
|
|
299
|
+
match Response::new(req.id.clone(), &result) {
|
|
300
|
+
Ok(r) => ResponseEnvelope::Success(r),
|
|
301
|
+
Err(e) => ResponseEnvelope::Error(ErrorResponse::new(
|
|
302
|
+
req.id,
|
|
303
|
+
ErrorObject::internal_error(e.to_string()),
|
|
304
|
+
)),
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn handle_prune_versions(req: Request) -> ResponseEnvelope {
|
|
309
|
+
let params = match req.params.as_ref() {
|
|
310
|
+
Some(p) => p,
|
|
311
|
+
None => {
|
|
312
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
313
|
+
req.id,
|
|
314
|
+
ErrorObject::invalid_params("Missing params"),
|
|
315
|
+
));
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
let package_name = match params["packageName"].as_str() {
|
|
320
|
+
Some(n) => n,
|
|
321
|
+
None => {
|
|
322
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
323
|
+
req.id,
|
|
324
|
+
ErrorObject::invalid_params("Missing packageName"),
|
|
325
|
+
));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
let keep_count = params["keepCount"].as_u64().unwrap_or(2) as u32;
|
|
330
|
+
|
|
331
|
+
let result = prune_versions(package_name, keep_count);
|
|
332
|
+
|
|
333
|
+
match Response::new(req.id.clone(), &result) {
|
|
334
|
+
Ok(r) => ResponseEnvelope::Success(r),
|
|
335
|
+
Err(e) => ResponseEnvelope::Error(ErrorResponse::new(
|
|
336
|
+
req.id,
|
|
337
|
+
ErrorObject::internal_error(e.to_string()),
|
|
338
|
+
)),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fn handle_remove_version(req: Request) -> ResponseEnvelope {
|
|
343
|
+
let params = match req.params.as_ref() {
|
|
344
|
+
Some(p) => p,
|
|
345
|
+
None => {
|
|
346
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
347
|
+
req.id,
|
|
348
|
+
ErrorObject::invalid_params("Missing params"),
|
|
349
|
+
));
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let package_name = match params["packageName"].as_str() {
|
|
354
|
+
Some(n) => n,
|
|
355
|
+
None => {
|
|
356
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
357
|
+
req.id,
|
|
358
|
+
ErrorObject::invalid_params("Missing packageName"),
|
|
359
|
+
));
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
let version = match params["version"].as_str() {
|
|
364
|
+
Some(v) => v,
|
|
365
|
+
None => {
|
|
366
|
+
return ResponseEnvelope::Error(ErrorResponse::new(
|
|
367
|
+
req.id,
|
|
368
|
+
ErrorObject::invalid_params("Missing version"),
|
|
369
|
+
));
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
let result = remove_version(package_name, version);
|
|
374
|
+
|
|
375
|
+
let response = match result {
|
|
376
|
+
Ok(msg) => serde_json::json!({
|
|
377
|
+
"success": true,
|
|
378
|
+
"message": msg
|
|
379
|
+
}),
|
|
380
|
+
Err(msg) => serde_json::json!({
|
|
381
|
+
"success": false,
|
|
382
|
+
"message": msg
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
match Response::new(req.id.clone(), &response) {
|
|
387
|
+
Ok(r) => ResponseEnvelope::Success(r),
|
|
388
|
+
Err(e) => ResponseEnvelope::Error(ErrorResponse::new(
|
|
389
|
+
req.id,
|
|
390
|
+
ErrorObject::internal_error(e.to_string()),
|
|
391
|
+
)),
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn handle_get_multi_version_packages(req: Request) -> ResponseEnvelope {
|
|
396
|
+
let packages = get_multi_version_packages();
|
|
397
|
+
|
|
398
|
+
match Response::new(req.id.clone(), &packages) {
|
|
399
|
+
Ok(r) => ResponseEnvelope::Success(r),
|
|
400
|
+
Err(e) => ResponseEnvelope::Error(ErrorResponse::new(
|
|
401
|
+
req.id,
|
|
402
|
+
ErrorObject::internal_error(e.to_string()),
|
|
403
|
+
)),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
230
407
|
// ---------------------------------------------------------------------------
|
|
231
408
|
// Router
|
|
232
409
|
// ---------------------------------------------------------------------------
|
|
@@ -247,6 +424,12 @@ fn route_request(request: Request) -> (ResponseEnvelope, bool) {
|
|
|
247
424
|
"scan" => handle_audit(request), // alias for audit
|
|
248
425
|
"sizes" => handle_sizes(request),
|
|
249
426
|
"installedInfo" => handle_installed_info(request),
|
|
427
|
+
// Multi-version methods
|
|
428
|
+
"listVersions" => handle_list_versions(request),
|
|
429
|
+
"switchVersion" => handle_switch_version(request),
|
|
430
|
+
"pruneVersions" => handle_prune_versions(request),
|
|
431
|
+
"removeVersion" => handle_remove_version(request),
|
|
432
|
+
"getMultiVersionPackages" => handle_get_multi_version_packages(request),
|
|
250
433
|
method => ResponseEnvelope::Error(ErrorResponse::new(
|
|
251
434
|
request.id,
|
|
252
435
|
ErrorObject::method_not_found(method),
|
package/src/index.ts
CHANGED
|
@@ -1109,8 +1109,10 @@ async function main(): Promise<void> {
|
|
|
1109
1109
|
await command.handler(commandArgs);
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
|
-
// Run CLI only when executed directly (not when imported)
|
|
1113
|
-
|
|
1112
|
+
// Run CLI only when executed directly via bin (not when imported as library)
|
|
1113
|
+
// This check works after bundling where import.meta.main gets transformed
|
|
1114
|
+
const isCliInvocation = process.argv[1]?.includes("pkg-ops");
|
|
1115
|
+
if (isCliInvocation) {
|
|
1114
1116
|
main().catch((error) => {
|
|
1115
1117
|
console.error("Fatal error:", error);
|
|
1116
1118
|
process.exit(1);
|