@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/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
  // ---------------------------------------------------------------------------
package/src/bridge.ts CHANGED
@@ -88,6 +88,44 @@ export interface InstalledPackageInfo {
88
88
  version: string;
89
89
  distSizeBytes: number | null;
90
90
  installedAt: string | null;
91
+ /** Total number of installed versions */
92
+ totalVersions?: number;
93
+ /** All installed versions */
94
+ versions?: VersionInfo[];
95
+ }
96
+
97
+ /**
98
+ * Version information for a package.
99
+ */
100
+ export interface VersionInfo {
101
+ version: string;
102
+ installedAt: string;
103
+ distSizeBytes: number | null;
104
+ fileCount: number | null;
105
+ active: boolean;
106
+ }
107
+
108
+ /**
109
+ * Result of switching versions.
110
+ */
111
+ export interface SwitchResult {
112
+ success: boolean;
113
+ packageName: string;
114
+ fromVersion: string;
115
+ toVersion: string;
116
+ message: string;
117
+ }
118
+
119
+ /**
120
+ * Result of pruning old versions.
121
+ */
122
+ export interface PruneResult {
123
+ success: boolean;
124
+ packageName: string;
125
+ removedVersions: string[];
126
+ keptVersions: string[];
127
+ freedBytes: number;
128
+ message: string;
91
129
  }
92
130
 
93
131
  interface JsonRpcRequest {
@@ -281,6 +319,55 @@ export class RustBridge {
281
319
  return this.sendRequest("installedInfo", {}) as Promise<InstalledPackageInfo[]>;
282
320
  }
283
321
 
322
+ // ---------------------------------------------------------------------------
323
+ // Multi-Version Methods
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /**
327
+ * List all installed versions of a package.
328
+ */
329
+ async listVersions(packageName: string): Promise<VersionInfo[]> {
330
+ return this.sendRequest("listVersions", { packageName }) as Promise<VersionInfo[]>;
331
+ }
332
+
333
+ /**
334
+ * Switch to a specific installed version.
335
+ */
336
+ async switchVersion(packageName: string, version: string): Promise<SwitchResult> {
337
+ return this.sendRequest("switchVersion", { packageName, version }) as Promise<SwitchResult>;
338
+ }
339
+
340
+ /**
341
+ * Remove old versions, keeping only the N most recent.
342
+ */
343
+ async pruneVersions(packageName: string, keepCount: number): Promise<PruneResult> {
344
+ return this.sendRequest("pruneVersions", { packageName, keepCount }) as Promise<PruneResult>;
345
+ }
346
+
347
+ /**
348
+ * Remove a specific version.
349
+ */
350
+ async removeVersion(packageName: string, version: string): Promise<{ success: boolean; message: string }> {
351
+ return this.sendRequest("removeVersion", { packageName, version }) as Promise<{ success: boolean; message: string }>;
352
+ }
353
+
354
+ /**
355
+ * Get packages with multiple versions installed.
356
+ */
357
+ async getMultiVersionPackages(): Promise<Array<{
358
+ packageName: string;
359
+ activeVersion: string;
360
+ totalVersions: number;
361
+ versions: string[];
362
+ }>> {
363
+ return this.sendRequest("getMultiVersionPackages", {}) as Promise<Array<{
364
+ packageName: string;
365
+ activeVersion: string;
366
+ totalVersions: number;
367
+ versions: string[];
368
+ }>>;
369
+ }
370
+
284
371
  // ---------------------------------------------------------------------------
285
372
  // Private Methods
286
373
  // ---------------------------------------------------------------------------
package/src/config.ts CHANGED
@@ -23,9 +23,23 @@ import { dirname } from "node:path";
23
23
  // Types
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
+ /**
27
+ * Metadata for a single installed version.
28
+ */
29
+ export interface VersionMetadata {
30
+ /** ISO timestamp when this version was installed */
31
+ installedAt: string;
32
+ /** Size of the dist directory in bytes */
33
+ distSizeBytes: number | null;
34
+ /** Number of files in dist */
35
+ fileCount: number | null;
36
+ }
37
+
26
38
  export interface PackageConfig {
27
- /** Package version (semver or "latest") */
39
+ /** Currently active version (semver) */
28
40
  version: string;
41
+ /** All installed versions with metadata */
42
+ versions: Record<string, VersionMetadata>;
29
43
  /** Associated systemd service name (without .service suffix) */
30
44
  service?: string;
31
45
  /** Whether to auto-start the service after install */
@@ -190,3 +204,161 @@ export function parsePackageSpec(spec: string): { name: string; version: string
190
204
 
191
205
  return { name, version };
192
206
  }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Multi-Version Helper Functions
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * Get all installed versions for a package.
214
+ */
215
+ export function getInstalledVersions(packageName: string): VersionMetadata[] {
216
+ const config = loadConfig();
217
+ const pkgConfig = config.packages[packageName];
218
+ if (!pkgConfig?.versions) {
219
+ return [];
220
+ }
221
+ return Object.entries(pkgConfig.versions).map(([version, meta]) => ({
222
+ version,
223
+ ...meta,
224
+ }));
225
+ }
226
+
227
+ /**
228
+ * Check if a specific version is installed.
229
+ */
230
+ export function isVersionInstalled(packageName: string, version: string): boolean {
231
+ const config = loadConfig();
232
+ const pkgConfig = config.packages[packageName];
233
+ return !!(pkgConfig?.versions?.[version]);
234
+ }
235
+
236
+ /**
237
+ * Get the active version for a package.
238
+ */
239
+ export function getActiveVersion(packageName: string): string | null {
240
+ const config = loadConfig();
241
+ const pkgConfig = config.packages[packageName];
242
+ return pkgConfig?.version ?? null;
243
+ }
244
+
245
+ /**
246
+ * Add a new version to the package config.
247
+ */
248
+ export function addPackageVersion(
249
+ packageName: string,
250
+ version: string,
251
+ metadata: Omit<VersionMetadata, "installedAt"> & { installedAt?: string }
252
+ ): void {
253
+ const config = loadConfig();
254
+
255
+ if (!config.packages[packageName]) {
256
+ config.packages[packageName] = {
257
+ version,
258
+ versions: {},
259
+ service: packageName.replace("@ebowwa/", ""),
260
+ autoStart: true,
261
+ };
262
+ }
263
+
264
+ config.packages[packageName].versions[version] = {
265
+ installedAt: metadata.installedAt ?? new Date().toISOString(),
266
+ distSizeBytes: metadata.distSizeBytes ?? null,
267
+ fileCount: metadata.fileCount ?? null,
268
+ };
269
+
270
+ saveConfig(config);
271
+ }
272
+
273
+ /**
274
+ * Remove a version from the package config.
275
+ * Returns true if the version was removed, false if it didn't exist.
276
+ */
277
+ export function removePackageVersion(packageName: string, version: string): boolean {
278
+ const config = loadConfig();
279
+ const pkgConfig = config.packages[packageName];
280
+
281
+ if (!pkgConfig?.versions?.[version]) {
282
+ return false;
283
+ }
284
+
285
+ delete pkgConfig.versions[version];
286
+
287
+ // If removing the active version, switch to the most recent remaining version
288
+ if (pkgConfig.version === version) {
289
+ const remainingVersions = Object.keys(pkgConfig.versions).sort((a, b) => {
290
+ // Sort by installedAt descending (most recent first)
291
+ const aTime = pkgConfig.versions[a]?.installedAt ?? "";
292
+ const bTime = pkgConfig.versions[b]?.installedAt ?? "";
293
+ return bTime.localeCompare(aTime);
294
+ });
295
+
296
+ if (remainingVersions.length > 0) {
297
+ pkgConfig.version = remainingVersions[0];
298
+ } else {
299
+ // No versions left, remove the package entirely
300
+ delete config.packages[packageName];
301
+ }
302
+ }
303
+
304
+ saveConfig(config);
305
+ return true;
306
+ }
307
+
308
+ /**
309
+ * Set the active version for a package.
310
+ * Returns true if successful, false if version not installed.
311
+ */
312
+ export function setActiveVersion(packageName: string, version: string): boolean {
313
+ const config = loadConfig();
314
+ const pkgConfig = config.packages[packageName];
315
+
316
+ if (!pkgConfig?.versions?.[version]) {
317
+ return false;
318
+ }
319
+
320
+ pkgConfig.version = version;
321
+ saveConfig(config);
322
+ return true;
323
+ }
324
+
325
+ /**
326
+ * Get the count of installed versions for a package.
327
+ */
328
+ export function getVersionCount(packageName: string): number {
329
+ const config = loadConfig();
330
+ const pkgConfig = config.packages[packageName];
331
+ return Object.keys(pkgConfig?.versions ?? {}).length;
332
+ }
333
+
334
+ /**
335
+ * Get packages with multiple versions installed.
336
+ */
337
+ export function getPackagesWithMultipleVersions(): Array<{
338
+ name: string;
339
+ activeVersion: string;
340
+ totalVersions: number;
341
+ versions: string[];
342
+ }> {
343
+ const config = loadConfig();
344
+ const result: Array<{
345
+ name: string;
346
+ activeVersion: string;
347
+ totalVersions: number;
348
+ versions: string[];
349
+ }> = [];
350
+
351
+ for (const [name, pkgConfig] of Object.entries(config.packages)) {
352
+ const versions = Object.keys(pkgConfig.versions ?? {});
353
+ if (versions.length > 1) {
354
+ result.push({
355
+ name,
356
+ activeVersion: pkgConfig.version,
357
+ totalVersions: versions.length,
358
+ versions: versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true })),
359
+ });
360
+ }
361
+ }
362
+
363
+ return result;
364
+ }