@ebowwa/pkg-ops 0.1.16 → 0.1.18

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/src/index.ts CHANGED
@@ -85,9 +85,18 @@ async function handleInstall(args: string[]): Promise<void> {
85
85
  console.log(` Previous version: ${result.previousVersion}`);
86
86
  }
87
87
 
88
- // Update config
88
+ // Update config (add version to versions map)
89
+ const existingConfig = getPackageConfig(name);
89
90
  updatePackageConfig(name, {
90
91
  version: result.version,
92
+ versions: {
93
+ ...existingConfig?.versions,
94
+ [result.version]: {
95
+ installedAt: new Date().toISOString(),
96
+ distSizeBytes: null,
97
+ fileCount: null,
98
+ },
99
+ },
91
100
  service: name.replace("@ebowwa/", ""),
92
101
  });
93
102
 
@@ -139,9 +148,18 @@ async function handleUpdate(args: string[]): Promise<void> {
139
148
  console.log(` Previous version: ${result.previousVersion}`);
140
149
  }
141
150
 
142
- // Update config
151
+ // Update config (add version to versions map)
152
+ const existingConfig = getPackageConfig(packageName);
143
153
  updatePackageConfig(packageName, {
144
154
  version: result.version,
155
+ versions: {
156
+ ...existingConfig?.versions,
157
+ [result.version]: {
158
+ installedAt: new Date().toISOString(),
159
+ distSizeBytes: null,
160
+ fileCount: null,
161
+ },
162
+ },
145
163
  service: packageName.replace("@ebowwa/", ""),
146
164
  });
147
165
 
@@ -487,6 +505,7 @@ async function handleConfigSet(args: string[]): Promise<void> {
487
505
 
488
506
  const config = getPackageConfig(packageName) ?? {
489
507
  version: "latest",
508
+ versions: {},
490
509
  };
491
510
 
492
511
  // Handle different value types
@@ -686,6 +705,230 @@ async function handleSizes(_args: string[]): Promise<void> {
686
705
  }
687
706
  }
688
707
 
708
+ // ---------------------------------------------------------------------------
709
+ // Multi-Version Commands
710
+ // ---------------------------------------------------------------------------
711
+
712
+ async function handleVersions(args: string[]): Promise<void> {
713
+ if (args.length === 0) {
714
+ console.error("Usage: pkg-ops versions <package>");
715
+ process.exit(1);
716
+ }
717
+
718
+ const packageName = args[0];
719
+
720
+ if (!isValidPackageName(packageName)) {
721
+ console.error(`Invalid package name. Only @ebowwa/* packages are supported.`);
722
+ process.exit(1);
723
+ }
724
+
725
+ try {
726
+ const bridge = await startBridge();
727
+ const versions = await bridge.listVersions(packageName);
728
+ await stopBridge();
729
+
730
+ if (versions.length === 0) {
731
+ console.log(`No versions of ${packageName} installed.`);
732
+ return;
733
+ }
734
+
735
+ console.log(`Installed versions for ${packageName}:\n`);
736
+
737
+ // Sort versions descending (newest first)
738
+ const sorted = [...versions].sort((a, b) => b.installedAt.localeCompare(a.installedAt));
739
+
740
+ for (const v of sorted) {
741
+ const active = v.active ? " [ACTIVE]" : "";
742
+ const size = v.distSizeBytes ? ` (${(v.distSizeBytes / 1024).toFixed(1)} KB)` : "";
743
+ const date = new Date(v.installedAt).toLocaleString();
744
+ console.log(` ${v.version}${active}${size}`);
745
+ console.log(` Installed: ${date}`);
746
+ if (v.fileCount) {
747
+ console.log(` Files: ${v.fileCount}`);
748
+ }
749
+ }
750
+
751
+ console.log(`\nTotal: ${versions.length} version(s)`);
752
+ } catch (error) {
753
+ console.error("Failed to list versions:", error);
754
+ process.exit(1);
755
+ } finally {
756
+ await stopBridge();
757
+ }
758
+ }
759
+
760
+ async function handleSwitch(args: string[]): Promise<void> {
761
+ if (args.length === 0) {
762
+ console.error("Usage: pkg-ops switch <package>@<version>");
763
+ process.exit(1);
764
+ }
765
+
766
+ const spec = args[0];
767
+ const { name, version } = parsePackageSpec(spec);
768
+
769
+ if (!isValidPackageName(name)) {
770
+ console.error(`Invalid package name. Only @ebowwa/* packages are supported.`);
771
+ process.exit(1);
772
+ }
773
+
774
+ if (!version || version === "latest") {
775
+ console.error("Error: Must specify a version to switch to (e.g., @ebowwa/stack@0.7.12)");
776
+ process.exit(1);
777
+ }
778
+
779
+ console.log(`Switching ${name} to version ${version}...`);
780
+
781
+ try {
782
+ const bridge = await startBridge();
783
+ const result = await bridge.switchVersion(name, version);
784
+
785
+ if (result.success) {
786
+ console.log(`Switched ${name} from ${result.fromVersion} to ${result.toVersion}`);
787
+
788
+ // Restart service if running
789
+ const sm = getServiceManager();
790
+ const config = getPackageConfig(name);
791
+ if (config?.service) {
792
+ const status = await sm.status(config.service);
793
+ if (status.active) {
794
+ await sm.restart(config.service);
795
+ console.log(` Service ${config.service} restarted`);
796
+ }
797
+ }
798
+ } else {
799
+ console.error(`Switch failed: ${result.message}`);
800
+ process.exit(1);
801
+ }
802
+ } catch (error) {
803
+ console.error("Switch failed:", error);
804
+ process.exit(1);
805
+ } finally {
806
+ await stopBridge();
807
+ }
808
+ }
809
+
810
+ async function handlePrune(args: string[]): Promise<void> {
811
+ if (args.length === 0) {
812
+ console.error("Usage: pkg-ops prune <package> [--keep N]");
813
+ process.exit(1);
814
+ }
815
+
816
+ const packageName = args[0];
817
+ const keepIndex = args.indexOf("--keep");
818
+ const keepCount = keepIndex >= 0 && args[keepIndex + 1]
819
+ ? parseInt(args[keepIndex + 1], 10)
820
+ : 2; // Default: keep 2 versions
821
+
822
+ if (!isValidPackageName(packageName)) {
823
+ console.error(`Invalid package name. Only @ebowwa/* packages are supported.`);
824
+ process.exit(1);
825
+ }
826
+
827
+ console.log(`Pruning ${packageName}, keeping ${keepCount} most recent version(s)...`);
828
+
829
+ try {
830
+ const bridge = await startBridge();
831
+ const result = await bridge.pruneVersions(packageName, keepCount);
832
+ await stopBridge();
833
+
834
+ if (result.success) {
835
+ if (result.removedVersions.length === 0) {
836
+ console.log(`No versions to remove (only ${result.keptVersions.length} installed).`);
837
+ } else {
838
+ console.log(`Removed ${result.removedVersions.length} version(s):`);
839
+ for (const v of result.removedVersions) {
840
+ console.log(` - ${v}`);
841
+ }
842
+ console.log(`Freed: ${(result.freedBytes / 1024).toFixed(1)} KB`);
843
+ console.log(`Kept: ${result.keptVersions.join(", ")}`);
844
+ }
845
+ } else {
846
+ console.error(`Prune failed: ${result.message}`);
847
+ process.exit(1);
848
+ }
849
+ } catch (error) {
850
+ console.error("Prune failed:", error);
851
+ process.exit(1);
852
+ } finally {
853
+ await stopBridge();
854
+ }
855
+ }
856
+
857
+ async function handleRemoveVersion(args: string[]): Promise<void> {
858
+ if (args.length === 0) {
859
+ console.error("Usage: pkg-ops remove-version <package>@<version>");
860
+ process.exit(1);
861
+ }
862
+
863
+ const spec = args[0];
864
+ const { name, version } = parsePackageSpec(spec);
865
+
866
+ if (!isValidPackageName(name)) {
867
+ console.error(`Invalid package name. Only @ebowwa/* packages are supported.`);
868
+ process.exit(1);
869
+ }
870
+
871
+ if (!version || version === "latest") {
872
+ console.error("Error: Must specify a version to remove (e.g., @ebowwa/stack@0.7.12)");
873
+ process.exit(1);
874
+ }
875
+
876
+ // Check if trying to remove active version
877
+ const config = getPackageConfig(name);
878
+ if (config?.version === version) {
879
+ console.error(`Error: Cannot remove active version ${version}. Switch to another version first.`);
880
+ process.exit(1);
881
+ }
882
+
883
+ console.log(`Removing ${name}@${version}...`);
884
+
885
+ try {
886
+ const bridge = await startBridge();
887
+ const result = await bridge.removeVersion(name, version);
888
+ await stopBridge();
889
+
890
+ if (result.success) {
891
+ console.log(`Removed ${name}@${version}`);
892
+ } else {
893
+ console.error(`Remove failed: ${result.message}`);
894
+ process.exit(1);
895
+ }
896
+ } catch (error) {
897
+ console.error("Remove failed:", error);
898
+ process.exit(1);
899
+ } finally {
900
+ await stopBridge();
901
+ }
902
+ }
903
+
904
+ async function handleMultiVersionList(_args: string[]): Promise<void> {
905
+ try {
906
+ const bridge = await startBridge();
907
+ const packages = await bridge.getMultiVersionPackages();
908
+ await stopBridge();
909
+
910
+ if (packages.length === 0) {
911
+ console.log("No packages with multiple versions installed.");
912
+ return;
913
+ }
914
+
915
+ console.log("Packages with multiple versions:\n");
916
+
917
+ for (const pkg of packages) {
918
+ console.log(` ${pkg.packageName}:`);
919
+ console.log(` Active: ${pkg.activeVersion}`);
920
+ console.log(` Total: ${pkg.totalVersions} version(s)`);
921
+ console.log(` Versions: ${pkg.versions.join(", ")}`);
922
+ console.log("");
923
+ }
924
+ } catch (error) {
925
+ console.error("Failed to list multi-version packages:", error);
926
+ process.exit(1);
927
+ } finally {
928
+ await stopBridge();
929
+ }
930
+ }
931
+
689
932
  // ---------------------------------------------------------------------------
690
933
  // CLI Entry Point
691
934
  // ---------------------------------------------------------------------------
@@ -697,6 +940,12 @@ const commands: Command[] = [
697
940
  { name: "update-all", description: "Update all managed packages", usage: "pkg-ops update-all", handler: handleUpdateAll },
698
941
  { name: "list", description: "List installed packages", usage: "pkg-ops list", handler: handleList },
699
942
  { name: "rollback", description: "Rollback a package to previous version", usage: "pkg-ops rollback <package>", handler: handleRollback },
943
+ // Multi-version management
944
+ { name: "versions", description: "List all installed versions of a package", usage: "pkg-ops versions <package>", handler: handleVersions },
945
+ { name: "switch", description: "Switch to a specific installed version", usage: "pkg-ops switch <package>@<version>", handler: handleSwitch },
946
+ { name: "prune", description: "Remove old versions, keeping N most recent", usage: "pkg-ops prune <package> [--keep N]", handler: handlePrune },
947
+ { name: "remove-version", description: "Remove a specific version", usage: "pkg-ops remove-version <package>@<version>", handler: handleRemoveVersion },
948
+ { name: "multi", description: "List packages with multiple versions", usage: "pkg-ops multi", handler: handleMultiVersionList },
700
949
  // Service management
701
950
  { name: "start", description: "Start a systemd service", usage: "pkg-ops service start <name>", handler: handleServiceStart },
702
951
  { name: "stop", description: "Stop a systemd service", usage: "pkg-ops service stop <name>", handler: handleServiceStop },
@@ -747,6 +996,13 @@ Package Management:
747
996
  list List installed packages
748
997
  rollback <package> Rollback a package to previous version
749
998
 
999
+ Multi-Version Management:
1000
+ versions <package> List all installed versions
1001
+ switch <package>@<version> Switch to a specific version
1002
+ prune <package> [--keep N] Remove old versions (default: keep 2)
1003
+ remove-version <pkg>@<ver> Remove a specific version
1004
+ multi List packages with multiple versions
1005
+
750
1006
  Service Management:
751
1007
  service start <name> Start a systemd service
752
1008
  service stop <name> Stop a systemd service
@@ -853,8 +1109,77 @@ async function main(): Promise<void> {
853
1109
  await command.handler(commandArgs);
854
1110
  }
855
1111
 
856
- // Run CLI
857
- main().catch((error) => {
858
- console.error("Fatal error:", error);
859
- process.exit(1);
860
- });
1112
+ // Run CLI only when executed directly (not when imported)
1113
+ if (import.meta.main) {
1114
+ main().catch((error) => {
1115
+ console.error("Fatal error:", error);
1116
+ process.exit(1);
1117
+ });
1118
+ }
1119
+
1120
+ // ---------------------------------------------------------------------------
1121
+ // Re-exports for library usage
1122
+ // ---------------------------------------------------------------------------
1123
+
1124
+ export {
1125
+ loadConfig,
1126
+ saveConfig,
1127
+ parsePackageSpec,
1128
+ isValidPackageName,
1129
+ getPackageConfig,
1130
+ updatePackageConfig,
1131
+ removePackageConfig,
1132
+ listManagedPackages,
1133
+ getConfigPath,
1134
+ ensureConfigDir,
1135
+ type PackageConfig,
1136
+ type PkgOpsConfig,
1137
+ } from "./config.js";
1138
+
1139
+ export {
1140
+ ServiceManager,
1141
+ getServiceManager,
1142
+ type ServiceInfo,
1143
+ type ServiceLogsOptions,
1144
+ } from "./service-manager.js";
1145
+
1146
+ export {
1147
+ RustBridge,
1148
+ startBridge,
1149
+ stopBridge,
1150
+ getBridge,
1151
+ type InstallResult,
1152
+ type PackageInfo,
1153
+ type RollbackResult,
1154
+ type VerifyResult,
1155
+ type AuditResult,
1156
+ type BundleSize,
1157
+ type InstalledPackageInfo,
1158
+ type VersionInfo,
1159
+ type SwitchResult,
1160
+ type PruneResult,
1161
+ } from "./bridge.js";
1162
+
1163
+ // Multi-version config helpers
1164
+ export {
1165
+ getInstalledVersions,
1166
+ isVersionInstalled,
1167
+ getActiveVersion,
1168
+ addPackageVersion,
1169
+ removePackageVersion,
1170
+ setActiveVersion,
1171
+ getVersionCount,
1172
+ getPackagesWithMultipleVersions,
1173
+ type VersionMetadata,
1174
+ } from "./config.js";
1175
+
1176
+ export {
1177
+ startHealthServer,
1178
+ stopHealthServer,
1179
+ getHealthServer,
1180
+ HealthServer,
1181
+ type HealthCheckResult,
1182
+ type ServiceHealth,
1183
+ type SystemHealth,
1184
+ } from "./health-server.js";
1185
+
package/src/types.ts ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Type definitions for PkgOps multi-version support.
3
+ *
4
+ * @module @ebowwa/pkg-ops/types
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Package Version Types
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * Metadata for an installed package version.
13
+ */
14
+ export interface VersionInfo {
15
+ /** Semver version string */
16
+ version: string;
17
+ /** ISO timestamp when this version was installed */
18
+ installedAt: string;
19
+ /** Size of the dist directory in bytes */
20
+ distSizeBytes: number | null;
21
+ /** Number of files in the dist directory */
22
+ fileCount: number | null;
23
+ /** Whether this is the currently active version */
24
+ active: boolean;
25
+ }
26
+
27
+ /**
28
+ * Configuration for a managed package with multi-version support.
29
+ */
30
+ export interface PackageConfig {
31
+ /** Currently active version */
32
+ version: string;
33
+ /** All installed versions with metadata */
34
+ versions: Record<string, VersionMetadata>;
35
+ /** Associated systemd service name (without .service suffix) */
36
+ service?: string;
37
+ /** Whether to auto-start the service after install */
38
+ autoStart?: boolean;
39
+ /** Custom environment variables for the service */
40
+ environment?: Record<string, string>;
41
+ /** Maximum number of versions to keep (default: 3) */
42
+ maxVersions?: number;
43
+ }
44
+
45
+ /**
46
+ * Metadata stored for each installed version.
47
+ */
48
+ export interface VersionMetadata {
49
+ /** ISO timestamp when this version was installed */
50
+ installedAt: string;
51
+ /** Size of the dist directory in bytes */
52
+ distSizeBytes: number | null;
53
+ /** Number of files in the dist directory */
54
+ fileCount: number | null;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Result Types
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Result of listing versions for a package.
63
+ */
64
+ export interface VersionsListResult {
65
+ packageName: string;
66
+ versions: VersionInfo[];
67
+ activeVersion: string;
68
+ maxVersions: number;
69
+ }
70
+
71
+ /**
72
+ * Result of switching to a different version.
73
+ */
74
+ export interface SwitchVersionResult {
75
+ success: boolean;
76
+ packageName: string;
77
+ previousVersion: string;
78
+ newVersion: string;
79
+ serviceRestarted: boolean;
80
+ message: string;
81
+ }
82
+
83
+ /**
84
+ * Result of pruning old versions.
85
+ */
86
+ export interface PruneResult {
87
+ success: boolean;
88
+ packageName: string;
89
+ removedVersions: string[];
90
+ keptVersions: string[];
91
+ freedBytes: number;
92
+ message: string;
93
+ }
94
+
95
+ /**
96
+ * Result of installing a package (updated for multi-version).
97
+ */
98
+ export interface InstallResult {
99
+ success: boolean;
100
+ version: string;
101
+ previousVersion?: string;
102
+ /** Whether this was a new version install or reinstall */
103
+ isNew: boolean;
104
+ /** Total versions now installed */
105
+ totalVersions: number;
106
+ message: string;
107
+ }
108
+
109
+ /**
110
+ * Result of rolling back a package.
111
+ */
112
+ export interface RollbackResult {
113
+ success: boolean;
114
+ previousVersion: string;
115
+ currentVersion: string;
116
+ /** Available versions to rollback to */
117
+ availableVersions: string[];
118
+ message: string;
119
+ }
120
+
121
+ /**
122
+ * Package info with version details.
123
+ */
124
+ export interface PackageInfo {
125
+ name: string;
126
+ version: string;
127
+ installed: boolean;
128
+ service?: string;
129
+ /** Total installed versions */
130
+ totalVersions?: number;
131
+ /** All installed versions */
132
+ versions?: VersionInfo[];
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Config Types
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Main configuration structure.
141
+ */
142
+ export interface PkgOpsConfig {
143
+ /** Managed packages */
144
+ packages: Record<string, PackageConfig>;
145
+ /** Health check HTTP port (default: 8914) */
146
+ healthPort?: number;
147
+ /** Working directory for installations (default: /root) */
148
+ workDir?: string;
149
+ /** Log level (default: "info") */
150
+ logLevel?: "debug" | "info" | "warn" | "error";
151
+ /** Default max versions to keep per package */
152
+ defaultMaxVersions?: number;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Bridge Types
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * JSON-RPC request structure.
161
+ */
162
+ export interface JsonRpcRequest {
163
+ jsonrpc: "2.0";
164
+ id: string;
165
+ method: string;
166
+ params?: Record<string, unknown>;
167
+ }
168
+
169
+ /**
170
+ * JSON-RPC response structure.
171
+ */
172
+ export interface JsonRpcResponse {
173
+ jsonrpc: "2.0";
174
+ id: string;
175
+ result?: unknown;
176
+ error?: {
177
+ code: number;
178
+ message: string;
179
+ data?: unknown;
180
+ };
181
+ }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Audit/Verify Types
185
+ // ---------------------------------------------------------------------------
186
+
187
+ /**
188
+ * Vulnerability audit result.
189
+ */
190
+ export interface AuditResult {
191
+ packageName: string;
192
+ severity: string;
193
+ vulnerability: string;
194
+ description: string;
195
+ }
196
+
197
+ /**
198
+ * Package verification result.
199
+ */
200
+ export interface VerifyResult {
201
+ packageName: string;
202
+ version: string;
203
+ success: boolean;
204
+ distExists: boolean;
205
+ checksum: string | null;
206
+ message: string;
207
+ }
208
+
209
+ /**
210
+ * Bundle size info.
211
+ */
212
+ export interface BundleSize {
213
+ packageName: string;
214
+ version: string;
215
+ distSizeBytes: number;
216
+ fileCount: number;
217
+ }
218
+
219
+ /**
220
+ * Installed package detailed info.
221
+ */
222
+ export interface InstalledPackageInfo {
223
+ packageName: string;
224
+ version: string;
225
+ distSizeBytes: number | null;
226
+ installedAt: string | null;
227
+ totalVersions?: number;
228
+ }
229
+
230
+ /**
231
+ * Health check result.
232
+ */
233
+ export interface HealthCheckResult {
234
+ healthy: boolean;
235
+ services: Array<{
236
+ name: string;
237
+ status: string;
238
+ pid: number;
239
+ }>;
240
+ }