@ebowwa/pkg-ops 0.1.20 → 0.1.21
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/package.json +1 -2
- package/rust/src/lib.rs +2 -0
- package/src/bridge.ts +0 -506
- package/src/config.ts +0 -364
- package/src/health-server.ts +0 -280
- package/src/index.ts +0 -1187
- package/src/service-manager.ts +0 -216
- package/src/types.ts +0 -240
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ebowwa/pkg-ops",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Package operations CLI - installs @ebowwa/* npm packages and manages systemd services",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"src",
|
|
18
17
|
"rust/src",
|
|
19
18
|
"rust/Cargo.toml",
|
|
20
19
|
"rust/Cargo.lock",
|
package/rust/src/lib.rs
CHANGED
package/src/bridge.ts
DELETED
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridge to Rust worker for package operations.
|
|
3
|
-
*
|
|
4
|
-
* Communicates with the Rust core via JSON-RPC 2.0 over stdin/stdout.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```typescript
|
|
8
|
-
* import { RustBridge } from "@ebowwa/pkg-ops/bridge";
|
|
9
|
-
*
|
|
10
|
-
* const bridge = new RustBridge();
|
|
11
|
-
* await bridge.start();
|
|
12
|
-
*
|
|
13
|
-
* // Install a package
|
|
14
|
-
* const result = await bridge.install("@ebowwa/stack", "0.7.12");
|
|
15
|
-
* console.log(result);
|
|
16
|
-
*
|
|
17
|
-
* // List packages
|
|
18
|
-
* const packages = await bridge.list();
|
|
19
|
-
*
|
|
20
|
-
* await bridge.stop();
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
25
|
-
import { accessSync, constants as fsConstants } from "fs";
|
|
26
|
-
import { join, dirname } from "path";
|
|
27
|
-
import { fileURLToPath } from "url";
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Types
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
export interface InstallResult {
|
|
34
|
-
success: boolean;
|
|
35
|
-
version: string;
|
|
36
|
-
previousVersion?: string;
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface PackageInfo {
|
|
41
|
-
name: string;
|
|
42
|
-
version: string;
|
|
43
|
-
installed: boolean;
|
|
44
|
-
service?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface RollbackResult {
|
|
48
|
-
success: boolean;
|
|
49
|
-
previousVersion: string;
|
|
50
|
-
currentVersion: string;
|
|
51
|
-
message: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface HealthCheckResult {
|
|
55
|
-
healthy: boolean;
|
|
56
|
-
services: Array<{
|
|
57
|
-
name: string;
|
|
58
|
-
status: string;
|
|
59
|
-
pid: number;
|
|
60
|
-
}>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface VerifyResult {
|
|
64
|
-
packageName: string;
|
|
65
|
-
version: string;
|
|
66
|
-
success: boolean;
|
|
67
|
-
distExists: boolean;
|
|
68
|
-
checksum: string | null;
|
|
69
|
-
message: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface AuditResult {
|
|
73
|
-
packageName: string;
|
|
74
|
-
severity: string;
|
|
75
|
-
vulnerability: string;
|
|
76
|
-
description: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface BundleSize {
|
|
80
|
-
packageName: string;
|
|
81
|
-
version: string;
|
|
82
|
-
distSizeBytes: number;
|
|
83
|
-
fileCount: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface InstalledPackageInfo {
|
|
87
|
-
packageName: string;
|
|
88
|
-
version: string;
|
|
89
|
-
distSizeBytes: number | null;
|
|
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;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
interface JsonRpcRequest {
|
|
132
|
-
jsonrpc: "2.0";
|
|
133
|
-
id: string;
|
|
134
|
-
method: string;
|
|
135
|
-
params?: Record<string, unknown>;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
interface JsonRpcResponse {
|
|
139
|
-
jsonrpc: "2.0";
|
|
140
|
-
id: string;
|
|
141
|
-
result?: unknown;
|
|
142
|
-
error?: {
|
|
143
|
-
code: number;
|
|
144
|
-
message: string;
|
|
145
|
-
data?: unknown;
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// Constants
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
const REQUEST_TIMEOUT = 30000;
|
|
154
|
-
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
// Rust Bridge Class
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Bridge to the Rust worker process.
|
|
161
|
-
*
|
|
162
|
-
* Handles JSON-RPC communication over stdin/stdout.
|
|
163
|
-
*/
|
|
164
|
-
export class RustBridge {
|
|
165
|
-
private process: ChildProcess | null = null;
|
|
166
|
-
private pendingRequests = new Map<string, {
|
|
167
|
-
resolve: (value: unknown) => void;
|
|
168
|
-
reject: (error: Error) => void;
|
|
169
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
170
|
-
}>();
|
|
171
|
-
private requestId = 0;
|
|
172
|
-
private buffer = "";
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Start the Rust worker process.
|
|
176
|
-
*/
|
|
177
|
-
async start(): Promise<void> {
|
|
178
|
-
if (this.process) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const binaryPath = this.getBinaryPath();
|
|
183
|
-
|
|
184
|
-
// Check if binary exists
|
|
185
|
-
try {
|
|
186
|
-
accessSync(binaryPath, fsConstants.X_OK);
|
|
187
|
-
} catch {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Rust binary not found at ${binaryPath}. ` +
|
|
190
|
-
`Run 'bun run build:rust' in the pkg-ops package.`
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this.process = spawn(binaryPath, [], {
|
|
195
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
if (!this.process.stdin || !this.process.stdout) {
|
|
199
|
-
throw new Error("Failed to create stdin/stdout pipes");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Handle stdout (JSON-RPC responses)
|
|
203
|
-
this.process.stdout.on("data", (data: Buffer) => {
|
|
204
|
-
this.buffer += data.toString("utf-8");
|
|
205
|
-
this.processBuffer();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Handle stderr (logs)
|
|
209
|
-
this.process.stderr?.on("data", (data: Buffer) => {
|
|
210
|
-
console.error("[rust-worker]", data.toString("utf-8").trim());
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Handle process exit
|
|
214
|
-
this.process.on("exit", (code: number | null) => {
|
|
215
|
-
console.error(`Rust worker exited with code ${code}`);
|
|
216
|
-
this.process = null;
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Stop the Rust worker process.
|
|
222
|
-
*/
|
|
223
|
-
async stop(): Promise<void> {
|
|
224
|
-
if (!this.process) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Send shutdown request
|
|
229
|
-
try {
|
|
230
|
-
await this.sendRequest("shutdown", {});
|
|
231
|
-
} catch {
|
|
232
|
-
// Ignore errors on shutdown
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Give it time to shutdown gracefully
|
|
236
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
237
|
-
|
|
238
|
-
// Force kill if still running
|
|
239
|
-
if (this.process) {
|
|
240
|
-
this.process.kill("SIGTERM");
|
|
241
|
-
this.process = null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Clear pending requests
|
|
245
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
246
|
-
clearTimeout(pending.timeout);
|
|
247
|
-
pending.reject(new Error("Bridge shutdown"));
|
|
248
|
-
}
|
|
249
|
-
this.pendingRequests.clear();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Install a package.
|
|
254
|
-
*/
|
|
255
|
-
async install(packageName: string, version: string): Promise<InstallResult> {
|
|
256
|
-
return this.sendRequest("install", { packageName, version }) as Promise<InstallResult>;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Update a package to latest version.
|
|
261
|
-
*/
|
|
262
|
-
async update(packageName: string): Promise<InstallResult> {
|
|
263
|
-
return this.sendRequest("update", { packageName }) as Promise<InstallResult>;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Update all managed packages.
|
|
268
|
-
*/
|
|
269
|
-
async updateAll(): Promise<InstallResult[]> {
|
|
270
|
-
return this.sendRequest("updateAll", {}) as Promise<InstallResult[]>;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* List installed packages.
|
|
275
|
-
*/
|
|
276
|
-
async list(): Promise<PackageInfo[]> {
|
|
277
|
-
return this.sendRequest("list", {}) as Promise<PackageInfo[]>;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Rollback a package to previous version.
|
|
282
|
-
*/
|
|
283
|
-
async rollback(packageName: string): Promise<RollbackResult> {
|
|
284
|
-
return this.sendRequest("rollback", { packageName }) as Promise<RollbackResult>;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Check health of all services.
|
|
289
|
-
*/
|
|
290
|
-
async health(): Promise<HealthCheckResult> {
|
|
291
|
-
return this.sendRequest("health", {}) as Promise<HealthCheckResult>;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Verify package integrity.
|
|
296
|
-
*/
|
|
297
|
-
async verify(packageName: string): Promise<VerifyResult> {
|
|
298
|
-
return this.sendRequest("verify", { packageName }) as Promise<VerifyResult>;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Run vulnerability scan.
|
|
303
|
-
*/
|
|
304
|
-
async audit(): Promise<AuditResult[]> {
|
|
305
|
-
return this.sendRequest("audit", {}) as Promise<AuditResult[]>;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Get bundle sizes.
|
|
310
|
-
*/
|
|
311
|
-
async getBundleSizes(): Promise<BundleSize[]> {
|
|
312
|
-
return this.sendRequest("sizes", {}) as Promise<BundleSize[]>;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Get detailed installed package info.
|
|
317
|
-
*/
|
|
318
|
-
async getInstalledInfo(): Promise<InstalledPackageInfo[]> {
|
|
319
|
-
return this.sendRequest("installedInfo", {}) as Promise<InstalledPackageInfo[]>;
|
|
320
|
-
}
|
|
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
|
-
|
|
371
|
-
// ---------------------------------------------------------------------------
|
|
372
|
-
// Private Methods
|
|
373
|
-
// ---------------------------------------------------------------------------
|
|
374
|
-
|
|
375
|
-
private getBinaryPath(): string {
|
|
376
|
-
// Get the directory of this module
|
|
377
|
-
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
378
|
-
|
|
379
|
-
// Paths to check (in order of preference)
|
|
380
|
-
const paths = [
|
|
381
|
-
// Cargo build output (works on VPS after `cargo build --release`)
|
|
382
|
-
join(currentDir, "..", "rust", "target", "release", "pkg-ops-core"),
|
|
383
|
-
// Pre-built binary in rust folder (for npm package distribution)
|
|
384
|
-
join(currentDir, "..", "rust", "pkg-ops-core"),
|
|
385
|
-
// Linux-specific cargo output
|
|
386
|
-
join(currentDir, "..", "rust", "target", "x86_64-unknown-linux-gnu", "release", "pkg-ops-core"),
|
|
387
|
-
];
|
|
388
|
-
|
|
389
|
-
for (const path of paths) {
|
|
390
|
-
try {
|
|
391
|
-
accessSync(path, fsConstants.X_OK);
|
|
392
|
-
return path;
|
|
393
|
-
} catch {
|
|
394
|
-
// Try next path
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Return the most likely path for error message
|
|
399
|
-
throw new Error(
|
|
400
|
-
`Rust binary not found. Tried:\n${paths.map(p => ` - ${p}`).join("\n")}\n` +
|
|
401
|
-
`Run 'bun run build:rust' or 'cargo build --release' in the pkg-ops package.`
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private sendRequest<T>(method: string, params: Record<string, unknown>): Promise<T> {
|
|
406
|
-
return new Promise((resolve, reject) => {
|
|
407
|
-
if (!this.process?.stdin) {
|
|
408
|
-
reject(new Error("Rust worker not started"));
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const id = String(++this.requestId);
|
|
413
|
-
const request: JsonRpcRequest = {
|
|
414
|
-
jsonrpc: "2.0",
|
|
415
|
-
id,
|
|
416
|
-
method,
|
|
417
|
-
params,
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
// Set up timeout
|
|
421
|
-
const timeout = setTimeout(() => {
|
|
422
|
-
this.pendingRequests.delete(id);
|
|
423
|
-
reject(new Error(`Request timeout: ${method}`));
|
|
424
|
-
}, REQUEST_TIMEOUT);
|
|
425
|
-
|
|
426
|
-
// Store pending request
|
|
427
|
-
this.pendingRequests.set(id, {
|
|
428
|
-
resolve: resolve as (value: unknown) => void,
|
|
429
|
-
reject,
|
|
430
|
-
timeout,
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Send request
|
|
434
|
-
const json = JSON.stringify(request) + "\n";
|
|
435
|
-
this.process.stdin.write(json, "utf-8", (err: Error | null | undefined) => {
|
|
436
|
-
if (err) {
|
|
437
|
-
this.pendingRequests.delete(id);
|
|
438
|
-
clearTimeout(timeout);
|
|
439
|
-
reject(err);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
private processBuffer(): void {
|
|
446
|
-
const lines = this.buffer.split("\n");
|
|
447
|
-
this.buffer = lines.pop() ?? "";
|
|
448
|
-
|
|
449
|
-
for (const line of lines) {
|
|
450
|
-
if (!line.trim()) continue;
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
const response: JsonRpcResponse = JSON.parse(line);
|
|
454
|
-
const pending = this.pendingRequests.get(response.id);
|
|
455
|
-
|
|
456
|
-
if (pending) {
|
|
457
|
-
clearTimeout(pending.timeout);
|
|
458
|
-
this.pendingRequests.delete(response.id);
|
|
459
|
-
|
|
460
|
-
if (response.error) {
|
|
461
|
-
pending.reject(new Error(response.error.message));
|
|
462
|
-
} else {
|
|
463
|
-
pending.resolve(response.result);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
} catch (err) {
|
|
467
|
-
console.error("Failed to parse response:", line, err);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ---------------------------------------------------------------------------
|
|
474
|
-
// Singleton Instance
|
|
475
|
-
// ---------------------------------------------------------------------------
|
|
476
|
-
|
|
477
|
-
let defaultBridge: RustBridge | null = null;
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Get the default bridge instance.
|
|
481
|
-
*/
|
|
482
|
-
export function getBridge(): RustBridge {
|
|
483
|
-
if (!defaultBridge) {
|
|
484
|
-
defaultBridge = new RustBridge();
|
|
485
|
-
}
|
|
486
|
-
return defaultBridge;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Start the default bridge.
|
|
491
|
-
*/
|
|
492
|
-
export async function startBridge(): Promise<RustBridge> {
|
|
493
|
-
const bridge = getBridge();
|
|
494
|
-
await bridge.start();
|
|
495
|
-
return bridge;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Stop the default bridge.
|
|
500
|
-
*/
|
|
501
|
-
export async function stopBridge(): Promise<void> {
|
|
502
|
-
if (defaultBridge) {
|
|
503
|
-
await defaultBridge.stop();
|
|
504
|
-
defaultBridge = null;
|
|
505
|
-
}
|
|
506
|
-
}
|