solforge 0.2.4 → 0.2.5
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/README.md +471 -79
- package/cli.cjs +106 -78
- package/package.json +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.cjs +66 -58
- package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
- package/server/ws-server.ts +4 -1
- package/src/api-server-entry.ts +91 -91
- package/src/cli/commands/rpc-start.ts +4 -1
- package/src/cli/main.ts +7 -3
- package/src/cli/run-solforge.ts +20 -6
- package/src/commands/add-program.ts +324 -328
- package/src/commands/init.ts +106 -106
- package/src/commands/list.ts +125 -125
- package/src/commands/mint.ts +246 -246
- package/src/commands/start.ts +834 -831
- package/src/commands/status.ts +80 -80
- package/src/commands/stop.ts +381 -382
- package/src/config/manager.ts +149 -149
- package/src/gui/public/app.css +1556 -1
- package/src/gui/public/build/main.css +1569 -1
- package/src/gui/server.ts +20 -21
- package/src/gui/src/app.tsx +56 -37
- package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
- package/src/gui/src/components/clone-program-modal.tsx +6 -6
- package/src/gui/src/components/clone-token-modal.tsx +7 -7
- package/src/gui/src/components/modal.tsx +13 -11
- package/src/gui/src/components/programs-panel.tsx +27 -15
- package/src/gui/src/components/status-panel.tsx +31 -17
- package/src/gui/src/components/tokens-panel.tsx +25 -19
- package/src/gui/src/index.css +491 -463
- package/src/index.ts +161 -146
- package/src/rpc/start.ts +1 -1
- package/src/services/api-server.ts +470 -473
- package/src/services/port-manager.ts +167 -167
- package/src/services/process-registry.ts +143 -143
- package/src/services/program-cloner.ts +312 -312
- package/src/services/token-cloner.ts +799 -797
- package/src/services/validator.ts +288 -288
- package/src/types/config.ts +71 -71
- package/src/utils/shell.ts +75 -75
- package/src/utils/token-loader.ts +77 -77
|
@@ -1,176 +1,176 @@
|
|
|
1
1
|
import { processRegistry } from "./process-registry.js";
|
|
2
2
|
|
|
3
3
|
export interface PortAllocation {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
rpcPort: number;
|
|
5
|
+
faucetPort: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export class PortManager {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
9
|
+
private readonly defaultRpcPort = 8899;
|
|
10
|
+
private readonly defaultFaucetPort = 9900;
|
|
11
|
+
private readonly portRangeStart = 8000;
|
|
12
|
+
private readonly portRangeEnd = 9999;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the next available port pair (RPC + Faucet)
|
|
16
|
+
*/
|
|
17
|
+
async getAvailablePorts(preferredRpcPort?: number): Promise<PortAllocation> {
|
|
18
|
+
const usedPorts = this.getUsedPorts();
|
|
19
|
+
|
|
20
|
+
// If preferred port is specified and available, use it
|
|
21
|
+
if (preferredRpcPort && !this.isPortUsed(preferredRpcPort, usedPorts)) {
|
|
22
|
+
const faucetPort = this.findAvailableFaucetPort(
|
|
23
|
+
preferredRpcPort,
|
|
24
|
+
usedPorts,
|
|
25
|
+
);
|
|
26
|
+
if (faucetPort) {
|
|
27
|
+
return { rpcPort: preferredRpcPort, faucetPort };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Otherwise, find the next available ports
|
|
32
|
+
return this.findNextAvailablePorts(usedPorts);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a specific port is available
|
|
37
|
+
*/
|
|
38
|
+
async isPortAvailable(port: number): Promise<boolean> {
|
|
39
|
+
const usedPorts = this.getUsedPorts();
|
|
40
|
+
return (
|
|
41
|
+
!this.isPortUsed(port, usedPorts) &&
|
|
42
|
+
(await this.checkPortActuallyFree(port))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get all currently used ports from running validators
|
|
48
|
+
*/
|
|
49
|
+
private getUsedPorts(): Set<number> {
|
|
50
|
+
const validators = processRegistry.getRunning();
|
|
51
|
+
const usedPorts = new Set<number>();
|
|
52
|
+
|
|
53
|
+
validators.forEach((validator) => {
|
|
54
|
+
usedPorts.add(validator.rpcPort);
|
|
55
|
+
usedPorts.add(validator.faucetPort);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return usedPorts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a port is in the used ports set
|
|
63
|
+
*/
|
|
64
|
+
private isPortUsed(port: number, usedPorts: Set<number>): boolean {
|
|
65
|
+
return usedPorts.has(port);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Find an available faucet port for a given RPC port
|
|
70
|
+
*/
|
|
71
|
+
private findAvailableFaucetPort(
|
|
72
|
+
rpcPort: number,
|
|
73
|
+
usedPorts: Set<number>,
|
|
74
|
+
): number | null {
|
|
75
|
+
// Try default offset first (faucet = rpc + 1001)
|
|
76
|
+
let faucetPort = rpcPort + 1001;
|
|
77
|
+
if (
|
|
78
|
+
!this.isPortUsed(faucetPort, usedPorts) &&
|
|
79
|
+
this.isPortInRange(faucetPort)
|
|
80
|
+
) {
|
|
81
|
+
return faucetPort;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Try other offsets
|
|
85
|
+
const offsets = [1000, 1002, 1003, 1004, 1005, 999, 998, 997];
|
|
86
|
+
for (const offset of offsets) {
|
|
87
|
+
faucetPort = rpcPort + offset;
|
|
88
|
+
if (
|
|
89
|
+
!this.isPortUsed(faucetPort, usedPorts) &&
|
|
90
|
+
this.isPortInRange(faucetPort)
|
|
91
|
+
) {
|
|
92
|
+
return faucetPort;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Search in the entire range
|
|
97
|
+
for (let port = this.portRangeStart; port <= this.portRangeEnd; port++) {
|
|
98
|
+
if (!this.isPortUsed(port, usedPorts)) {
|
|
99
|
+
return port;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find the next available port pair
|
|
108
|
+
*/
|
|
109
|
+
private findNextAvailablePorts(usedPorts: Set<number>): PortAllocation {
|
|
110
|
+
// Start from default ports if available
|
|
111
|
+
if (!this.isPortUsed(this.defaultRpcPort, usedPorts)) {
|
|
112
|
+
const faucetPort = this.findAvailableFaucetPort(
|
|
113
|
+
this.defaultRpcPort,
|
|
114
|
+
usedPorts,
|
|
115
|
+
);
|
|
116
|
+
if (faucetPort) {
|
|
117
|
+
return { rpcPort: this.defaultRpcPort, faucetPort };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Search for available RPC port
|
|
122
|
+
for (
|
|
123
|
+
let rpcPort = this.portRangeStart;
|
|
124
|
+
rpcPort <= this.portRangeEnd;
|
|
125
|
+
rpcPort++
|
|
126
|
+
) {
|
|
127
|
+
if (!this.isPortUsed(rpcPort, usedPorts)) {
|
|
128
|
+
const faucetPort = this.findAvailableFaucetPort(rpcPort, usedPorts);
|
|
129
|
+
if (faucetPort) {
|
|
130
|
+
return { rpcPort, faucetPort };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new Error("No available port pairs found in the specified range");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if port is within allowed range
|
|
140
|
+
*/
|
|
141
|
+
private isPortInRange(port: number): boolean {
|
|
142
|
+
return port >= this.portRangeStart && port <= this.portRangeEnd;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Actually check if a port is free by attempting to bind to it
|
|
147
|
+
*/
|
|
148
|
+
private async checkPortActuallyFree(port: number): Promise<boolean> {
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
const net = require("net");
|
|
151
|
+
const server = net.createServer();
|
|
152
|
+
|
|
153
|
+
server.listen(port, (err: any) => {
|
|
154
|
+
if (err) {
|
|
155
|
+
resolve(false);
|
|
156
|
+
} else {
|
|
157
|
+
server.once("close", () => resolve(true));
|
|
158
|
+
server.close();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.on("error", () => resolve(false));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get recommended ports for a configuration
|
|
168
|
+
*/
|
|
169
|
+
async getRecommendedPorts(config: {
|
|
170
|
+
localnet: { port: number; faucetPort: number };
|
|
171
|
+
}): Promise<PortAllocation> {
|
|
172
|
+
return this.getAvailablePorts(config.localnet.port);
|
|
173
|
+
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// Singleton instance
|
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
2
|
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
4
|
import type { Config } from "../types/config.js";
|
|
5
5
|
|
|
6
6
|
export interface RunningValidator {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
pid: number;
|
|
10
|
+
rpcPort: number;
|
|
11
|
+
faucetPort: number;
|
|
12
|
+
rpcUrl: string;
|
|
13
|
+
faucetUrl: string;
|
|
14
|
+
configPath: string;
|
|
15
|
+
startTime: Date;
|
|
16
|
+
status: "running" | "stopped" | "error";
|
|
17
|
+
apiServerPort?: number;
|
|
18
|
+
apiServerUrl?: string;
|
|
19
|
+
apiServerPid?: number;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class ProcessRegistry {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
23
|
+
private registryPath: string;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
// Store registry in user's home directory
|
|
27
|
+
this.registryPath = join(homedir(), ".solforge", "running-validators.json");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all running validators
|
|
32
|
+
*/
|
|
33
|
+
getRunning(): RunningValidator[] {
|
|
34
|
+
if (!existsSync(this.registryPath)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = readFileSync(this.registryPath, "utf-8");
|
|
40
|
+
const validators = JSON.parse(content) as RunningValidator[];
|
|
41
|
+
|
|
42
|
+
// Convert startTime strings back to Date objects
|
|
43
|
+
return validators.map((v) => ({
|
|
44
|
+
...v,
|
|
45
|
+
startTime: new Date(v.startTime),
|
|
46
|
+
}));
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register a new running validator
|
|
54
|
+
*/
|
|
55
|
+
register(validator: RunningValidator): void {
|
|
56
|
+
const validators = this.getRunning();
|
|
57
|
+
|
|
58
|
+
// Remove any existing entry with the same ID
|
|
59
|
+
const updated = validators.filter((v) => v.id !== validator.id);
|
|
60
|
+
updated.push(validator);
|
|
61
|
+
|
|
62
|
+
this.save(updated);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Unregister a validator
|
|
67
|
+
*/
|
|
68
|
+
unregister(id: string): void {
|
|
69
|
+
const validators = this.getRunning();
|
|
70
|
+
const updated = validators.filter((v) => v.id !== id);
|
|
71
|
+
this.save(updated);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update validator status
|
|
76
|
+
*/
|
|
77
|
+
updateStatus(id: string, status: RunningValidator["status"]): void {
|
|
78
|
+
const validators = this.getRunning();
|
|
79
|
+
const validator = validators.find((v) => v.id === id);
|
|
80
|
+
|
|
81
|
+
if (validator) {
|
|
82
|
+
validator.status = status;
|
|
83
|
+
this.save(validators);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get validator by ID
|
|
89
|
+
*/
|
|
90
|
+
getById(id: string): RunningValidator | undefined {
|
|
91
|
+
return this.getRunning().find((v) => v.id === id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get validator by PID
|
|
96
|
+
*/
|
|
97
|
+
getByPid(pid: number): RunningValidator | undefined {
|
|
98
|
+
return this.getRunning().find((v) => v.pid === pid);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get validator by port
|
|
103
|
+
*/
|
|
104
|
+
getByPort(port: number): RunningValidator | undefined {
|
|
105
|
+
return this.getRunning().find(
|
|
106
|
+
(v) => v.rpcPort === port || v.faucetPort === port,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a process is actually running
|
|
112
|
+
*/
|
|
113
|
+
async isProcessRunning(pid: number): Promise<boolean> {
|
|
114
|
+
try {
|
|
115
|
+
// Send signal 0 to check if process exists
|
|
116
|
+
process.kill(pid, 0);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clean up dead processes from registry
|
|
125
|
+
*/
|
|
126
|
+
async cleanup(): Promise<void> {
|
|
127
|
+
const validators = this.getRunning();
|
|
128
|
+
const active: RunningValidator[] = [];
|
|
129
|
+
|
|
130
|
+
for (const validator of validators) {
|
|
131
|
+
if (await this.isProcessRunning(validator.pid)) {
|
|
132
|
+
active.push(validator);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.save(active);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save validators to registry file
|
|
141
|
+
*/
|
|
142
|
+
private save(validators: RunningValidator[]): void {
|
|
143
|
+
// Ensure directory exists
|
|
144
|
+
const dir = join(homedir(), ".solforge");
|
|
145
|
+
if (!existsSync(dir)) {
|
|
146
|
+
require("fs").mkdirSync(dir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
writeFileSync(this.registryPath, JSON.stringify(validators, null, 2));
|
|
150
|
+
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// Singleton instance
|