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.
Files changed (42) hide show
  1. package/README.md +471 -79
  2. package/cli.cjs +106 -78
  3. package/package.json +1 -1
  4. package/scripts/install.sh +1 -1
  5. package/scripts/postinstall.cjs +66 -58
  6. package/server/methods/program/get-token-accounts-by-owner.ts +7 -2
  7. package/server/ws-server.ts +4 -1
  8. package/src/api-server-entry.ts +91 -91
  9. package/src/cli/commands/rpc-start.ts +4 -1
  10. package/src/cli/main.ts +7 -3
  11. package/src/cli/run-solforge.ts +20 -6
  12. package/src/commands/add-program.ts +324 -328
  13. package/src/commands/init.ts +106 -106
  14. package/src/commands/list.ts +125 -125
  15. package/src/commands/mint.ts +246 -246
  16. package/src/commands/start.ts +834 -831
  17. package/src/commands/status.ts +80 -80
  18. package/src/commands/stop.ts +381 -382
  19. package/src/config/manager.ts +149 -149
  20. package/src/gui/public/app.css +1556 -1
  21. package/src/gui/public/build/main.css +1569 -1
  22. package/src/gui/server.ts +20 -21
  23. package/src/gui/src/app.tsx +56 -37
  24. package/src/gui/src/components/airdrop-mint-form.tsx +17 -11
  25. package/src/gui/src/components/clone-program-modal.tsx +6 -6
  26. package/src/gui/src/components/clone-token-modal.tsx +7 -7
  27. package/src/gui/src/components/modal.tsx +13 -11
  28. package/src/gui/src/components/programs-panel.tsx +27 -15
  29. package/src/gui/src/components/status-panel.tsx +31 -17
  30. package/src/gui/src/components/tokens-panel.tsx +25 -19
  31. package/src/gui/src/index.css +491 -463
  32. package/src/index.ts +161 -146
  33. package/src/rpc/start.ts +1 -1
  34. package/src/services/api-server.ts +470 -473
  35. package/src/services/port-manager.ts +167 -167
  36. package/src/services/process-registry.ts +143 -143
  37. package/src/services/program-cloner.ts +312 -312
  38. package/src/services/token-cloner.ts +799 -797
  39. package/src/services/validator.ts +288 -288
  40. package/src/types/config.ts +71 -71
  41. package/src/utils/shell.ts +75 -75
  42. package/src/utils/token-loader.ts +77 -77
@@ -1,406 +1,405 @@
1
- import chalk from "chalk";
2
1
  import { select } from "@inquirer/prompts";
3
- import { processRegistry } from "../services/process-registry.js";
4
-
2
+ import chalk from "chalk";
5
3
  import type { RunningValidator } from "../services/process-registry.js";
4
+ import { processRegistry } from "../services/process-registry.js";
6
5
 
7
6
  export async function stopCommand(
8
- validatorId?: string,
9
- options: { all?: boolean; kill?: boolean } = {}
7
+ validatorId?: string,
8
+ options: { all?: boolean; kill?: boolean } = {},
10
9
  ): Promise<void> {
11
- console.log(chalk.blue("🛑 Stopping validator(s)...\n"));
12
-
13
- // Clean up dead processes first
14
- await processRegistry.cleanup();
15
-
16
- const validators = processRegistry.getRunning();
17
-
18
- if (validators.length === 0) {
19
- console.log(chalk.yellow("⚠️ No running validators found"));
20
- return;
21
- }
22
-
23
- let validatorsToStop: RunningValidator[] = [];
24
-
25
- if (options.all) {
26
- // Stop all validators
27
- validatorsToStop = validators;
28
- console.log(
29
- chalk.cyan(`🔄 Stopping all ${validators.length} validator(s)...`)
30
- );
31
- } else if (validatorId) {
32
- // Stop specific validator
33
- const validator = processRegistry.getById(validatorId);
34
- if (!validator) {
35
- console.error(
36
- chalk.red(`❌ Validator with ID '${validatorId}' not found`)
37
- );
38
- console.log(
39
- chalk.gray("💡 Use `solforge list` to see running validators")
40
- );
41
- return;
42
- }
43
- validatorsToStop = [validator];
44
- console.log(
45
- chalk.cyan(
46
- `🔄 Stopping validator '${validator.name}' (${validatorId})...`
47
- )
48
- );
49
- } else {
50
- // No specific validator specified, show available options
51
- console.log(chalk.yellow("⚠️ Please specify which validator to stop:"));
52
- console.log(
53
- chalk.gray("💡 Use `solforge stop <id>` to stop a specific validator")
54
- );
55
- console.log(
56
- chalk.gray("💡 Use `solforge stop --all` to stop all validators")
57
- );
58
- console.log(chalk.gray("💡 Use `solforge list` to see running validators"));
59
- return;
60
- }
61
-
62
- // Stop each validator
63
- let stoppedCount = 0;
64
- let errorCount = 0;
65
-
66
- for (const validator of validatorsToStop) {
67
- try {
68
- const result = await stopValidator(validator, options.kill);
69
- if (result.success) {
70
- console.log(
71
- chalk.green(`✅ Stopped ${validator.name} (${validator.id})`)
72
- );
73
- stoppedCount++;
74
- } else {
75
- console.error(
76
- chalk.red(
77
- `❌ Failed to stop ${validator.name} (${validator.id}): ${result.error}`
78
- )
79
- );
80
- errorCount++;
81
- }
82
- } catch (error) {
83
- console.error(
84
- chalk.red(
85
- `❌ Error stopping ${validator.name} (${validator.id}): ${
86
- error instanceof Error ? error.message : String(error)
87
- }`
88
- )
89
- );
90
- errorCount++;
91
- }
92
- }
93
-
94
- // Summary
95
- console.log();
96
- if (stoppedCount > 0) {
97
- console.log(
98
- chalk.green(`✅ Successfully stopped ${stoppedCount} validator(s)`)
99
- );
100
- }
101
- if (errorCount > 0) {
102
- console.log(chalk.red(`❌ Failed to stop ${errorCount} validator(s)`));
103
- }
10
+ console.log(chalk.blue("🛑 Stopping validator(s)...\n"));
11
+
12
+ // Clean up dead processes first
13
+ await processRegistry.cleanup();
14
+
15
+ const validators = processRegistry.getRunning();
16
+
17
+ if (validators.length === 0) {
18
+ console.log(chalk.yellow("⚠️ No running validators found"));
19
+ return;
20
+ }
21
+
22
+ let validatorsToStop: RunningValidator[] = [];
23
+
24
+ if (options.all) {
25
+ // Stop all validators
26
+ validatorsToStop = validators;
27
+ console.log(
28
+ chalk.cyan(`🔄 Stopping all ${validators.length} validator(s)...`),
29
+ );
30
+ } else if (validatorId) {
31
+ // Stop specific validator
32
+ const validator = processRegistry.getById(validatorId);
33
+ if (!validator) {
34
+ console.error(
35
+ chalk.red(`❌ Validator with ID '${validatorId}' not found`),
36
+ );
37
+ console.log(
38
+ chalk.gray("💡 Use `solforge list` to see running validators"),
39
+ );
40
+ return;
41
+ }
42
+ validatorsToStop = [validator];
43
+ console.log(
44
+ chalk.cyan(
45
+ `🔄 Stopping validator '${validator.name}' (${validatorId})...`,
46
+ ),
47
+ );
48
+ } else {
49
+ // No specific validator specified, show available options
50
+ console.log(chalk.yellow("⚠️ Please specify which validator to stop:"));
51
+ console.log(
52
+ chalk.gray("💡 Use `solforge stop <id>` to stop a specific validator"),
53
+ );
54
+ console.log(
55
+ chalk.gray("💡 Use `solforge stop --all` to stop all validators"),
56
+ );
57
+ console.log(chalk.gray("💡 Use `solforge list` to see running validators"));
58
+ return;
59
+ }
60
+
61
+ // Stop each validator
62
+ let stoppedCount = 0;
63
+ let errorCount = 0;
64
+
65
+ for (const validator of validatorsToStop) {
66
+ try {
67
+ const result = await stopValidator(validator, options.kill);
68
+ if (result.success) {
69
+ console.log(
70
+ chalk.green(`✅ Stopped ${validator.name} (${validator.id})`),
71
+ );
72
+ stoppedCount++;
73
+ } else {
74
+ console.error(
75
+ chalk.red(
76
+ `❌ Failed to stop ${validator.name} (${validator.id}): ${result.error}`,
77
+ ),
78
+ );
79
+ errorCount++;
80
+ }
81
+ } catch (error) {
82
+ console.error(
83
+ chalk.red(
84
+ `❌ Error stopping ${validator.name} (${validator.id}): ${
85
+ error instanceof Error ? error.message : String(error)
86
+ }`,
87
+ ),
88
+ );
89
+ errorCount++;
90
+ }
91
+ }
92
+
93
+ // Summary
94
+ console.log();
95
+ if (stoppedCount > 0) {
96
+ console.log(
97
+ chalk.green(`✅ Successfully stopped ${stoppedCount} validator(s)`),
98
+ );
99
+ }
100
+ if (errorCount > 0) {
101
+ console.log(chalk.red(`❌ Failed to stop ${errorCount} validator(s)`));
102
+ }
104
103
  }
105
104
 
106
105
  async function stopValidator(
107
- validator: RunningValidator,
108
- forceKill: boolean = false
106
+ validator: RunningValidator,
107
+ forceKill: boolean = false,
109
108
  ): Promise<{ success: boolean; error?: string }> {
110
- try {
111
- // Stop the API server first if it has a PID
112
- if (validator.apiServerPid) {
113
- try {
114
- process.kill(validator.apiServerPid, "SIGTERM");
115
- console.log(
116
- chalk.gray(`📡 Stopped API server (PID: ${validator.apiServerPid})`)
117
- );
118
- } catch (error) {
119
- console.log(
120
- chalk.yellow(
121
- `⚠️ Warning: Failed to stop API server: ${
122
- error instanceof Error ? error.message : String(error)
123
- }`
124
- )
125
- );
126
- }
127
- }
128
-
129
- // Check if process is still running
130
- const isRunning = await processRegistry.isProcessRunning(validator.pid);
131
- if (!isRunning) {
132
- // Process already stopped, just clean up registry
133
- processRegistry.unregister(validator.id);
134
- return { success: true };
135
- }
136
-
137
- const signal = forceKill ? "SIGKILL" : "SIGTERM";
138
-
139
- // Send termination signal
140
- process.kill(validator.pid, signal);
141
-
142
- if (forceKill) {
143
- // For SIGKILL, process should stop immediately
144
- processRegistry.unregister(validator.id);
145
- return { success: true };
146
- } else {
147
- // For SIGTERM, wait for graceful shutdown
148
- const shutdownResult = await waitForProcessShutdown(validator.pid, 10000);
149
-
150
- if (shutdownResult.success) {
151
- processRegistry.unregister(validator.id);
152
- return { success: true };
153
- } else {
154
- // Graceful shutdown failed, try force kill
155
- console.log(
156
- chalk.yellow(
157
- `⚠️ Graceful shutdown failed for ${validator.name}, force killing...`
158
- )
159
- );
160
- process.kill(validator.pid, "SIGKILL");
161
- processRegistry.unregister(validator.id);
162
- return { success: true };
163
- }
164
- }
165
- } catch (error) {
166
- const errorMessage = error instanceof Error ? error.message : String(error);
167
-
168
- // If error is "ESRCH" (No such process), the process is already gone
169
- if (errorMessage.includes("ESRCH")) {
170
- processRegistry.unregister(validator.id);
171
- return { success: true };
172
- }
173
-
174
- return { success: false, error: errorMessage };
175
- }
109
+ try {
110
+ // Stop the API server first if it has a PID
111
+ if (validator.apiServerPid) {
112
+ try {
113
+ process.kill(validator.apiServerPid, "SIGTERM");
114
+ console.log(
115
+ chalk.gray(`📡 Stopped API server (PID: ${validator.apiServerPid})`),
116
+ );
117
+ } catch (error) {
118
+ console.log(
119
+ chalk.yellow(
120
+ `⚠️ Warning: Failed to stop API server: ${
121
+ error instanceof Error ? error.message : String(error)
122
+ }`,
123
+ ),
124
+ );
125
+ }
126
+ }
127
+
128
+ // Check if process is still running
129
+ const isRunning = await processRegistry.isProcessRunning(validator.pid);
130
+ if (!isRunning) {
131
+ // Process already stopped, just clean up registry
132
+ processRegistry.unregister(validator.id);
133
+ return { success: true };
134
+ }
135
+
136
+ const signal = forceKill ? "SIGKILL" : "SIGTERM";
137
+
138
+ // Send termination signal
139
+ process.kill(validator.pid, signal);
140
+
141
+ if (forceKill) {
142
+ // For SIGKILL, process should stop immediately
143
+ processRegistry.unregister(validator.id);
144
+ return { success: true };
145
+ } else {
146
+ // For SIGTERM, wait for graceful shutdown
147
+ const shutdownResult = await waitForProcessShutdown(validator.pid, 10000);
148
+
149
+ if (shutdownResult.success) {
150
+ processRegistry.unregister(validator.id);
151
+ return { success: true };
152
+ } else {
153
+ // Graceful shutdown failed, try force kill
154
+ console.log(
155
+ chalk.yellow(
156
+ `⚠️ Graceful shutdown failed for ${validator.name}, force killing...`,
157
+ ),
158
+ );
159
+ process.kill(validator.pid, "SIGKILL");
160
+ processRegistry.unregister(validator.id);
161
+ return { success: true };
162
+ }
163
+ }
164
+ } catch (error) {
165
+ const errorMessage = error instanceof Error ? error.message : String(error);
166
+
167
+ // If error is "ESRCH" (No such process), the process is already gone
168
+ if (errorMessage.includes("ESRCH")) {
169
+ processRegistry.unregister(validator.id);
170
+ return { success: true };
171
+ }
172
+
173
+ return { success: false, error: errorMessage };
174
+ }
176
175
  }
177
176
 
178
177
  async function waitForProcessShutdown(
179
- pid: number,
180
- timeoutMs: number = 10000
178
+ pid: number,
179
+ timeoutMs: number = 10000,
181
180
  ): Promise<{ success: boolean; error?: string }> {
182
- const startTime = Date.now();
183
-
184
- while (Date.now() - startTime < timeoutMs) {
185
- try {
186
- // Send signal 0 to check if process exists
187
- process.kill(pid, 0);
188
- // If no error thrown, process is still running
189
- await new Promise((resolve) => setTimeout(resolve, 500));
190
- } catch (error) {
191
- // Process is gone
192
- return { success: true };
193
- }
194
- }
195
-
196
- return { success: false, error: "Process shutdown timeout" };
181
+ const startTime = Date.now();
182
+
183
+ while (Date.now() - startTime < timeoutMs) {
184
+ try {
185
+ // Send signal 0 to check if process exists
186
+ process.kill(pid, 0);
187
+ // If no error thrown, process is still running
188
+ await new Promise((resolve) => setTimeout(resolve, 500));
189
+ } catch (error) {
190
+ // Process is gone
191
+ return { success: true };
192
+ }
193
+ }
194
+
195
+ return { success: false, error: "Process shutdown timeout" };
197
196
  }
198
197
 
199
198
  export async function killCommand(
200
- validatorId?: string,
201
- options: { all?: boolean } = {}
199
+ validatorId?: string,
200
+ options: { all?: boolean } = {},
202
201
  ): Promise<void> {
203
- console.log(chalk.red("💀 Force killing validator(s)...\n"));
204
-
205
- // Clean up dead processes first
206
- await processRegistry.cleanup();
207
-
208
- const validators = processRegistry.getRunning();
209
-
210
- if (validators.length === 0) {
211
- console.log(chalk.yellow("⚠️ No running validators found"));
212
- return;
213
- }
214
-
215
- let validatorsToKill: RunningValidator[] = [];
216
-
217
- if (options.all) {
218
- // Kill all validators
219
- validatorsToKill = validators;
220
- console.log(
221
- chalk.cyan(`🔄 Force killing all ${validators.length} validator(s)...`)
222
- );
223
- } else if (validatorId) {
224
- // Kill specific validator
225
- const validator = processRegistry.getById(validatorId);
226
- if (!validator) {
227
- console.error(
228
- chalk.red(`❌ Validator with ID '${validatorId}' not found`)
229
- );
230
- console.log(
231
- chalk.gray("💡 Use `solforge list` to see running validators")
232
- );
233
- return;
234
- }
235
- validatorsToKill = [validator];
236
- console.log(
237
- chalk.cyan(
238
- `🔄 Force killing validator '${validator.name}' (${validatorId})...`
239
- )
240
- );
241
- } else {
242
- // No specific validator specified, show interactive selection
243
- console.log(chalk.cyan("📋 Select validator(s) to force kill:\n"));
244
-
245
- // Display current validators
246
- displayValidatorsTable(validators);
247
-
248
- const choices = [
249
- ...validators.map((v) => ({
250
- name: `${v.name} (${v.id}) - PID: ${v.pid}`,
251
- value: v.id,
252
- })),
253
- {
254
- name: chalk.red("Kill ALL validators"),
255
- value: "all",
256
- },
257
- {
258
- name: chalk.gray("Cancel"),
259
- value: "cancel",
260
- },
261
- ];
262
-
263
- const selectedValidator = await select({
264
- message: "Which validator would you like to force kill?",
265
- choices,
266
- });
267
-
268
- if (selectedValidator === "cancel") {
269
- console.log(chalk.gray("Operation cancelled"));
270
- return;
271
- }
272
-
273
- if (selectedValidator === "all") {
274
- validatorsToKill = validators;
275
- console.log(
276
- chalk.cyan(`🔄 Force killing all ${validators.length} validator(s)...`)
277
- );
278
- } else {
279
- const validator = processRegistry.getById(selectedValidator);
280
- if (!validator) {
281
- console.error(chalk.red("❌ Selected validator not found"));
282
- return;
283
- }
284
- validatorsToKill = [validator];
285
- console.log(
286
- chalk.cyan(
287
- `🔄 Force killing validator '${validator.name}' (${selectedValidator})...`
288
- )
289
- );
290
- }
291
- }
292
-
293
- // Kill each validator
294
- let killedCount = 0;
295
- let errorCount = 0;
296
-
297
- for (const validator of validatorsToKill) {
298
- try {
299
- const result = await stopValidator(validator, true); // Force kill
300
- if (result.success) {
301
- console.log(
302
- chalk.green(`✅ Killed ${validator.name} (${validator.id})`)
303
- );
304
- killedCount++;
305
- } else {
306
- console.error(
307
- chalk.red(
308
- `❌ Failed to kill ${validator.name} (${validator.id}): ${result.error}`
309
- )
310
- );
311
- errorCount++;
312
- }
313
- } catch (error) {
314
- console.error(
315
- chalk.red(
316
- `❌ Error killing ${validator.name} (${validator.id}): ${
317
- error instanceof Error ? error.message : String(error)
318
- }`
319
- )
320
- );
321
- errorCount++;
322
- }
323
- }
324
-
325
- // Summary
326
- console.log();
327
- if (killedCount > 0) {
328
- console.log(
329
- chalk.green(`✅ Successfully killed ${killedCount} validator(s)`)
330
- );
331
- }
332
- if (errorCount > 0) {
333
- console.log(chalk.red(`❌ Failed to kill ${errorCount} validator(s)`));
334
- }
202
+ console.log(chalk.red("💀 Force killing validator(s)...\n"));
203
+
204
+ // Clean up dead processes first
205
+ await processRegistry.cleanup();
206
+
207
+ const validators = processRegistry.getRunning();
208
+
209
+ if (validators.length === 0) {
210
+ console.log(chalk.yellow("⚠️ No running validators found"));
211
+ return;
212
+ }
213
+
214
+ let validatorsToKill: RunningValidator[] = [];
215
+
216
+ if (options.all) {
217
+ // Kill all validators
218
+ validatorsToKill = validators;
219
+ console.log(
220
+ chalk.cyan(`🔄 Force killing all ${validators.length} validator(s)...`),
221
+ );
222
+ } else if (validatorId) {
223
+ // Kill specific validator
224
+ const validator = processRegistry.getById(validatorId);
225
+ if (!validator) {
226
+ console.error(
227
+ chalk.red(`❌ Validator with ID '${validatorId}' not found`),
228
+ );
229
+ console.log(
230
+ chalk.gray("💡 Use `solforge list` to see running validators"),
231
+ );
232
+ return;
233
+ }
234
+ validatorsToKill = [validator];
235
+ console.log(
236
+ chalk.cyan(
237
+ `🔄 Force killing validator '${validator.name}' (${validatorId})...`,
238
+ ),
239
+ );
240
+ } else {
241
+ // No specific validator specified, show interactive selection
242
+ console.log(chalk.cyan("📋 Select validator(s) to force kill:\n"));
243
+
244
+ // Display current validators
245
+ displayValidatorsTable(validators);
246
+
247
+ const choices = [
248
+ ...validators.map((v) => ({
249
+ name: `${v.name} (${v.id}) - PID: ${v.pid}`,
250
+ value: v.id,
251
+ })),
252
+ {
253
+ name: chalk.red("Kill ALL validators"),
254
+ value: "all",
255
+ },
256
+ {
257
+ name: chalk.gray("Cancel"),
258
+ value: "cancel",
259
+ },
260
+ ];
261
+
262
+ const selectedValidator = await select({
263
+ message: "Which validator would you like to force kill?",
264
+ choices,
265
+ });
266
+
267
+ if (selectedValidator === "cancel") {
268
+ console.log(chalk.gray("Operation cancelled"));
269
+ return;
270
+ }
271
+
272
+ if (selectedValidator === "all") {
273
+ validatorsToKill = validators;
274
+ console.log(
275
+ chalk.cyan(`🔄 Force killing all ${validators.length} validator(s)...`),
276
+ );
277
+ } else {
278
+ const validator = processRegistry.getById(selectedValidator);
279
+ if (!validator) {
280
+ console.error(chalk.red("❌ Selected validator not found"));
281
+ return;
282
+ }
283
+ validatorsToKill = [validator];
284
+ console.log(
285
+ chalk.cyan(
286
+ `🔄 Force killing validator '${validator.name}' (${selectedValidator})...`,
287
+ ),
288
+ );
289
+ }
290
+ }
291
+
292
+ // Kill each validator
293
+ let killedCount = 0;
294
+ let errorCount = 0;
295
+
296
+ for (const validator of validatorsToKill) {
297
+ try {
298
+ const result = await stopValidator(validator, true); // Force kill
299
+ if (result.success) {
300
+ console.log(
301
+ chalk.green(`✅ Killed ${validator.name} (${validator.id})`),
302
+ );
303
+ killedCount++;
304
+ } else {
305
+ console.error(
306
+ chalk.red(
307
+ `❌ Failed to kill ${validator.name} (${validator.id}): ${result.error}`,
308
+ ),
309
+ );
310
+ errorCount++;
311
+ }
312
+ } catch (error) {
313
+ console.error(
314
+ chalk.red(
315
+ `❌ Error killing ${validator.name} (${validator.id}): ${
316
+ error instanceof Error ? error.message : String(error)
317
+ }`,
318
+ ),
319
+ );
320
+ errorCount++;
321
+ }
322
+ }
323
+
324
+ // Summary
325
+ console.log();
326
+ if (killedCount > 0) {
327
+ console.log(
328
+ chalk.green(`✅ Successfully killed ${killedCount} validator(s)`),
329
+ );
330
+ }
331
+ if (errorCount > 0) {
332
+ console.log(chalk.red(`❌ Failed to kill ${errorCount} validator(s)`));
333
+ }
335
334
  }
336
335
 
337
336
  function displayValidatorsTable(validators: RunningValidator[]): void {
338
- // Calculate column widths
339
- const maxIdWidth = Math.max(2, ...validators.map((v) => v.id.length));
340
- const maxNameWidth = Math.max(4, ...validators.map((v) => v.name.length));
341
- const maxPidWidth = Math.max(
342
- 3,
343
- ...validators.map((v) => v.pid.toString().length)
344
- );
345
- const maxPortWidth = 9; // "8899/9900" format
346
- const maxUptimeWidth = 7;
347
-
348
- // Header
349
- const header =
350
- chalk.bold("ID".padEnd(maxIdWidth)) +
351
- " | " +
352
- chalk.bold("Name".padEnd(maxNameWidth)) +
353
- " | " +
354
- chalk.bold("PID".padEnd(maxPidWidth)) +
355
- " | " +
356
- chalk.bold("RPC/Faucet".padEnd(maxPortWidth)) +
357
- " | " +
358
- chalk.bold("Uptime".padEnd(maxUptimeWidth)) +
359
- " | " +
360
- chalk.bold("Status");
361
-
362
- console.log(header);
363
- console.log("-".repeat(header.length - 20)); // Subtract ANSI codes length
364
-
365
- // Rows
366
- validators.forEach((validator) => {
367
- const uptime = formatUptime(validator.startTime);
368
- const ports = `${validator.rpcPort}/${validator.faucetPort}`;
369
- const status =
370
- validator.status === "running" ? chalk.green("●") : chalk.red("●");
371
-
372
- const row =
373
- validator.id.padEnd(maxIdWidth) +
374
- " | " +
375
- validator.name.padEnd(maxNameWidth) +
376
- " | " +
377
- validator.pid.toString().padEnd(maxPidWidth) +
378
- " | " +
379
- ports.padEnd(maxPortWidth) +
380
- " | " +
381
- uptime.padEnd(maxUptimeWidth) +
382
- " | " +
383
- status;
384
-
385
- console.log(row);
386
- });
387
-
388
- console.log(); // Empty line
337
+ // Calculate column widths
338
+ const maxIdWidth = Math.max(2, ...validators.map((v) => v.id.length));
339
+ const maxNameWidth = Math.max(4, ...validators.map((v) => v.name.length));
340
+ const maxPidWidth = Math.max(
341
+ 3,
342
+ ...validators.map((v) => v.pid.toString().length),
343
+ );
344
+ const maxPortWidth = 9; // "8899/9900" format
345
+ const maxUptimeWidth = 7;
346
+
347
+ // Header
348
+ const header =
349
+ chalk.bold("ID".padEnd(maxIdWidth)) +
350
+ " | " +
351
+ chalk.bold("Name".padEnd(maxNameWidth)) +
352
+ " | " +
353
+ chalk.bold("PID".padEnd(maxPidWidth)) +
354
+ " | " +
355
+ chalk.bold("RPC/Faucet".padEnd(maxPortWidth)) +
356
+ " | " +
357
+ chalk.bold("Uptime".padEnd(maxUptimeWidth)) +
358
+ " | " +
359
+ chalk.bold("Status");
360
+
361
+ console.log(header);
362
+ console.log("-".repeat(header.length - 20)); // Subtract ANSI codes length
363
+
364
+ // Rows
365
+ validators.forEach((validator) => {
366
+ const uptime = formatUptime(validator.startTime);
367
+ const ports = `${validator.rpcPort}/${validator.faucetPort}`;
368
+ const status =
369
+ validator.status === "running" ? chalk.green("●") : chalk.red("●");
370
+
371
+ const row =
372
+ validator.id.padEnd(maxIdWidth) +
373
+ " | " +
374
+ validator.name.padEnd(maxNameWidth) +
375
+ " | " +
376
+ validator.pid.toString().padEnd(maxPidWidth) +
377
+ " | " +
378
+ ports.padEnd(maxPortWidth) +
379
+ " | " +
380
+ uptime.padEnd(maxUptimeWidth) +
381
+ " | " +
382
+ status;
383
+
384
+ console.log(row);
385
+ });
386
+
387
+ console.log(); // Empty line
389
388
  }
390
389
 
391
390
  function formatUptime(startTime: Date): string {
392
- const now = new Date();
393
- const uptimeMs = now.getTime() - startTime.getTime();
394
- const uptimeSeconds = Math.floor(uptimeMs / 1000);
395
-
396
- if (uptimeSeconds < 60) {
397
- return `${uptimeSeconds}s`;
398
- } else if (uptimeSeconds < 3600) {
399
- const minutes = Math.floor(uptimeSeconds / 60);
400
- return `${minutes}m`;
401
- } else {
402
- const hours = Math.floor(uptimeSeconds / 3600);
403
- const minutes = Math.floor((uptimeSeconds % 3600) / 60);
404
- return `${hours}h${minutes}m`;
405
- }
391
+ const now = new Date();
392
+ const uptimeMs = now.getTime() - startTime.getTime();
393
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
394
+
395
+ if (uptimeSeconds < 60) {
396
+ return `${uptimeSeconds}s`;
397
+ } else if (uptimeSeconds < 3600) {
398
+ const minutes = Math.floor(uptimeSeconds / 60);
399
+ return `${minutes}m`;
400
+ } else {
401
+ const hours = Math.floor(uptimeSeconds / 3600);
402
+ const minutes = Math.floor((uptimeSeconds % 3600) / 60);
403
+ return `${hours}h${minutes}m`;
404
+ }
406
405
  }