@ebowwa/pkg-ops 0.1.17 → 0.1.19
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/bridge.d.ts +63 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/config.d.ts +55 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -14
- package/dist/index.js.map +7 -9
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +2 -2
- package/rust/src/lib.rs +438 -0
- package/rust/src/main.rs +177 -0
- package/src/bridge.ts +87 -0
- package/src/config.ts +173 -1
- package/src/index.ts +283 -7
- package/src/types.ts +240 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for PkgOps multi-version support.
|
|
3
|
+
*
|
|
4
|
+
* @module @ebowwa/pkg-ops/types
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Metadata for an installed package version.
|
|
8
|
+
*/
|
|
9
|
+
export interface VersionInfo {
|
|
10
|
+
/** Semver version string */
|
|
11
|
+
version: string;
|
|
12
|
+
/** ISO timestamp when this version was installed */
|
|
13
|
+
installedAt: string;
|
|
14
|
+
/** Size of the dist directory in bytes */
|
|
15
|
+
distSizeBytes: number | null;
|
|
16
|
+
/** Number of files in the dist directory */
|
|
17
|
+
fileCount: number | null;
|
|
18
|
+
/** Whether this is the currently active version */
|
|
19
|
+
active: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for a managed package with multi-version support.
|
|
23
|
+
*/
|
|
24
|
+
export interface PackageConfig {
|
|
25
|
+
/** Currently active version */
|
|
26
|
+
version: string;
|
|
27
|
+
/** All installed versions with metadata */
|
|
28
|
+
versions: Record<string, VersionMetadata>;
|
|
29
|
+
/** Associated systemd service name (without .service suffix) */
|
|
30
|
+
service?: string;
|
|
31
|
+
/** Whether to auto-start the service after install */
|
|
32
|
+
autoStart?: boolean;
|
|
33
|
+
/** Custom environment variables for the service */
|
|
34
|
+
environment?: Record<string, string>;
|
|
35
|
+
/** Maximum number of versions to keep (default: 3) */
|
|
36
|
+
maxVersions?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Metadata stored for each installed version.
|
|
40
|
+
*/
|
|
41
|
+
export interface VersionMetadata {
|
|
42
|
+
/** ISO timestamp when this version was installed */
|
|
43
|
+
installedAt: string;
|
|
44
|
+
/** Size of the dist directory in bytes */
|
|
45
|
+
distSizeBytes: number | null;
|
|
46
|
+
/** Number of files in the dist directory */
|
|
47
|
+
fileCount: number | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result of listing versions for a package.
|
|
51
|
+
*/
|
|
52
|
+
export interface VersionsListResult {
|
|
53
|
+
packageName: string;
|
|
54
|
+
versions: VersionInfo[];
|
|
55
|
+
activeVersion: string;
|
|
56
|
+
maxVersions: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Result of switching to a different version.
|
|
60
|
+
*/
|
|
61
|
+
export interface SwitchVersionResult {
|
|
62
|
+
success: boolean;
|
|
63
|
+
packageName: string;
|
|
64
|
+
previousVersion: string;
|
|
65
|
+
newVersion: string;
|
|
66
|
+
serviceRestarted: boolean;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Result of pruning old versions.
|
|
71
|
+
*/
|
|
72
|
+
export interface PruneResult {
|
|
73
|
+
success: boolean;
|
|
74
|
+
packageName: string;
|
|
75
|
+
removedVersions: string[];
|
|
76
|
+
keptVersions: string[];
|
|
77
|
+
freedBytes: number;
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Result of installing a package (updated for multi-version).
|
|
82
|
+
*/
|
|
83
|
+
export interface InstallResult {
|
|
84
|
+
success: boolean;
|
|
85
|
+
version: string;
|
|
86
|
+
previousVersion?: string;
|
|
87
|
+
/** Whether this was a new version install or reinstall */
|
|
88
|
+
isNew: boolean;
|
|
89
|
+
/** Total versions now installed */
|
|
90
|
+
totalVersions: number;
|
|
91
|
+
message: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Result of rolling back a package.
|
|
95
|
+
*/
|
|
96
|
+
export interface RollbackResult {
|
|
97
|
+
success: boolean;
|
|
98
|
+
previousVersion: string;
|
|
99
|
+
currentVersion: string;
|
|
100
|
+
/** Available versions to rollback to */
|
|
101
|
+
availableVersions: string[];
|
|
102
|
+
message: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Package info with version details.
|
|
106
|
+
*/
|
|
107
|
+
export interface PackageInfo {
|
|
108
|
+
name: string;
|
|
109
|
+
version: string;
|
|
110
|
+
installed: boolean;
|
|
111
|
+
service?: string;
|
|
112
|
+
/** Total installed versions */
|
|
113
|
+
totalVersions?: number;
|
|
114
|
+
/** All installed versions */
|
|
115
|
+
versions?: VersionInfo[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Main configuration structure.
|
|
119
|
+
*/
|
|
120
|
+
export interface PkgOpsConfig {
|
|
121
|
+
/** Managed packages */
|
|
122
|
+
packages: Record<string, PackageConfig>;
|
|
123
|
+
/** Health check HTTP port (default: 8914) */
|
|
124
|
+
healthPort?: number;
|
|
125
|
+
/** Working directory for installations (default: /root) */
|
|
126
|
+
workDir?: string;
|
|
127
|
+
/** Log level (default: "info") */
|
|
128
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
129
|
+
/** Default max versions to keep per package */
|
|
130
|
+
defaultMaxVersions?: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* JSON-RPC request structure.
|
|
134
|
+
*/
|
|
135
|
+
export interface JsonRpcRequest {
|
|
136
|
+
jsonrpc: "2.0";
|
|
137
|
+
id: string;
|
|
138
|
+
method: string;
|
|
139
|
+
params?: Record<string, unknown>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* JSON-RPC response structure.
|
|
143
|
+
*/
|
|
144
|
+
export interface JsonRpcResponse {
|
|
145
|
+
jsonrpc: "2.0";
|
|
146
|
+
id: string;
|
|
147
|
+
result?: unknown;
|
|
148
|
+
error?: {
|
|
149
|
+
code: number;
|
|
150
|
+
message: string;
|
|
151
|
+
data?: unknown;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Vulnerability audit result.
|
|
156
|
+
*/
|
|
157
|
+
export interface AuditResult {
|
|
158
|
+
packageName: string;
|
|
159
|
+
severity: string;
|
|
160
|
+
vulnerability: string;
|
|
161
|
+
description: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Package verification result.
|
|
165
|
+
*/
|
|
166
|
+
export interface VerifyResult {
|
|
167
|
+
packageName: string;
|
|
168
|
+
version: string;
|
|
169
|
+
success: boolean;
|
|
170
|
+
distExists: boolean;
|
|
171
|
+
checksum: string | null;
|
|
172
|
+
message: string;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Bundle size info.
|
|
176
|
+
*/
|
|
177
|
+
export interface BundleSize {
|
|
178
|
+
packageName: string;
|
|
179
|
+
version: string;
|
|
180
|
+
distSizeBytes: number;
|
|
181
|
+
fileCount: number;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Installed package detailed info.
|
|
185
|
+
*/
|
|
186
|
+
export interface InstalledPackageInfo {
|
|
187
|
+
packageName: string;
|
|
188
|
+
version: string;
|
|
189
|
+
distSizeBytes: number | null;
|
|
190
|
+
installedAt: string | null;
|
|
191
|
+
totalVersions?: number;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Health check result.
|
|
195
|
+
*/
|
|
196
|
+
export interface HealthCheckResult {
|
|
197
|
+
healthy: boolean;
|
|
198
|
+
services: Array<{
|
|
199
|
+
name: string;
|
|
200
|
+
status: string;
|
|
201
|
+
pid: number;
|
|
202
|
+
}>;
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebowwa/pkg-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Package operations CLI - installs @ebowwa/* npm packages and manages systemd services",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "bun build src/index.ts --outdir dist --target
|
|
24
|
+
"build": "bun build src/index.ts --outdir dist --target bun --sourcemap --minify && tsc --emitDeclarationOnly --declaration",
|
|
25
25
|
"build:rust": "cd rust && cargo build --release",
|
|
26
26
|
"dev": "bun run src/index.ts",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
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
|
+
}
|