servherd 0.0.1 → 1.0.0

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 (95) hide show
  1. package/CONTRIBUTING.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +653 -29
  4. package/dist/cli/commands/config.d.ts +35 -0
  5. package/dist/cli/commands/config.js +336 -0
  6. package/dist/cli/commands/info.d.ts +37 -0
  7. package/dist/cli/commands/info.js +98 -0
  8. package/dist/cli/commands/list.d.ts +26 -0
  9. package/dist/cli/commands/list.js +86 -0
  10. package/dist/cli/commands/logs.d.ts +46 -0
  11. package/dist/cli/commands/logs.js +292 -0
  12. package/dist/cli/commands/mcp.d.ts +5 -0
  13. package/dist/cli/commands/mcp.js +17 -0
  14. package/dist/cli/commands/refresh.d.ts +20 -0
  15. package/dist/cli/commands/refresh.js +139 -0
  16. package/dist/cli/commands/remove.d.ts +20 -0
  17. package/dist/cli/commands/remove.js +144 -0
  18. package/dist/cli/commands/restart.d.ts +25 -0
  19. package/dist/cli/commands/restart.js +177 -0
  20. package/dist/cli/commands/start.d.ts +37 -0
  21. package/dist/cli/commands/start.js +293 -0
  22. package/dist/cli/commands/stop.d.ts +20 -0
  23. package/dist/cli/commands/stop.js +108 -0
  24. package/dist/cli/index.d.ts +9 -0
  25. package/dist/cli/index.js +160 -0
  26. package/dist/cli/output/formatters.d.ts +117 -0
  27. package/dist/cli/output/formatters.js +454 -0
  28. package/dist/cli/output/json-formatter.d.ts +22 -0
  29. package/dist/cli/output/json-formatter.js +40 -0
  30. package/dist/index.d.ts +15 -0
  31. package/dist/index.js +25 -0
  32. package/dist/mcp/index.d.ts +14 -0
  33. package/dist/mcp/index.js +352 -0
  34. package/dist/mcp/resources/servers.d.ts +14 -0
  35. package/dist/mcp/resources/servers.js +128 -0
  36. package/dist/mcp/tools/config.d.ts +33 -0
  37. package/dist/mcp/tools/config.js +88 -0
  38. package/dist/mcp/tools/info.d.ts +36 -0
  39. package/dist/mcp/tools/info.js +65 -0
  40. package/dist/mcp/tools/list.d.ts +36 -0
  41. package/dist/mcp/tools/list.js +49 -0
  42. package/dist/mcp/tools/logs.d.ts +44 -0
  43. package/dist/mcp/tools/logs.js +55 -0
  44. package/dist/mcp/tools/refresh.d.ts +33 -0
  45. package/dist/mcp/tools/refresh.js +54 -0
  46. package/dist/mcp/tools/remove.d.ts +23 -0
  47. package/dist/mcp/tools/remove.js +43 -0
  48. package/dist/mcp/tools/restart.d.ts +23 -0
  49. package/dist/mcp/tools/restart.js +42 -0
  50. package/dist/mcp/tools/start.d.ts +38 -0
  51. package/dist/mcp/tools/start.js +73 -0
  52. package/dist/mcp/tools/stop.d.ts +23 -0
  53. package/dist/mcp/tools/stop.js +40 -0
  54. package/dist/services/config.service.d.ts +80 -0
  55. package/dist/services/config.service.js +227 -0
  56. package/dist/services/port.service.d.ts +82 -0
  57. package/dist/services/port.service.js +151 -0
  58. package/dist/services/process.service.d.ts +61 -0
  59. package/dist/services/process.service.js +220 -0
  60. package/dist/services/registry.service.d.ts +50 -0
  61. package/dist/services/registry.service.js +157 -0
  62. package/dist/types/config.d.ts +107 -0
  63. package/dist/types/config.js +44 -0
  64. package/dist/types/errors.d.ts +102 -0
  65. package/dist/types/errors.js +197 -0
  66. package/dist/types/pm2.d.ts +50 -0
  67. package/dist/types/pm2.js +4 -0
  68. package/dist/types/registry.d.ts +230 -0
  69. package/dist/types/registry.js +33 -0
  70. package/dist/utils/ci-detector.d.ts +31 -0
  71. package/dist/utils/ci-detector.js +68 -0
  72. package/dist/utils/config-drift.d.ts +71 -0
  73. package/dist/utils/config-drift.js +128 -0
  74. package/dist/utils/error-handler.d.ts +21 -0
  75. package/dist/utils/error-handler.js +38 -0
  76. package/dist/utils/log-follower.d.ts +10 -0
  77. package/dist/utils/log-follower.js +98 -0
  78. package/dist/utils/logger.d.ts +11 -0
  79. package/dist/utils/logger.js +24 -0
  80. package/dist/utils/names.d.ts +7 -0
  81. package/dist/utils/names.js +20 -0
  82. package/dist/utils/template.d.ts +88 -0
  83. package/dist/utils/template.js +180 -0
  84. package/dist/utils/time-parser.d.ts +19 -0
  85. package/dist/utils/time-parser.js +54 -0
  86. package/docs/ci-cd.md +408 -0
  87. package/docs/configuration.md +325 -0
  88. package/docs/mcp-integration.md +411 -0
  89. package/examples/basic-usage/README.md +187 -0
  90. package/examples/ci-github-actions/workflow.yml +195 -0
  91. package/examples/mcp-claude-code/README.md +213 -0
  92. package/examples/multi-server/README.md +270 -0
  93. package/examples/storybook/README.md +187 -0
  94. package/examples/vite-project/README.md +251 -0
  95. package/package.json +123 -6
@@ -0,0 +1,177 @@
1
+ import { RegistryService } from "../../services/registry.service.js";
2
+ import { ProcessService } from "../../services/process.service.js";
3
+ import { ConfigService } from "../../services/config.service.js";
4
+ import { formatRestartResult, formatError } from "../output/formatters.js";
5
+ import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ import { renderTemplate, renderEnvTemplates, getTemplateVariables } from "../../utils/template.js";
8
+ import { extractUsedConfigKeys, createConfigSnapshot, detectDrift, } from "../../utils/config-drift.js";
9
+ /**
10
+ * Re-resolve a server's command template with current config values
11
+ * Updates the registry with new resolved command and config snapshot
12
+ */
13
+ async function refreshServerConfig(server, config, registryService) {
14
+ // Get template variables with current config
15
+ const templateVars = getTemplateVariables(config, server.port);
16
+ // Re-resolve the command template
17
+ const resolvedCommand = renderTemplate(server.command, templateVars);
18
+ // Re-resolve environment variables if any
19
+ const resolvedEnv = server.env
20
+ ? renderEnvTemplates(server.env, templateVars)
21
+ : {};
22
+ // Extract new used config keys and create new snapshot
23
+ const usedConfigKeys = extractUsedConfigKeys(server.command);
24
+ const configSnapshot = createConfigSnapshot(config, usedConfigKeys);
25
+ // Update the registry
26
+ await registryService.updateServer(server.id, {
27
+ resolvedCommand,
28
+ env: resolvedEnv,
29
+ usedConfigKeys,
30
+ configSnapshot,
31
+ });
32
+ return { resolvedCommand, configSnapshot };
33
+ }
34
+ /**
35
+ * Execute the restart command for a single server
36
+ */
37
+ export async function executeRestart(options) {
38
+ const registryService = new RegistryService();
39
+ const processService = new ProcessService();
40
+ const configService = new ConfigService();
41
+ try {
42
+ // Load registry and config
43
+ await registryService.load();
44
+ const config = await configService.load();
45
+ // Connect to PM2
46
+ await processService.connect();
47
+ // Determine which servers to restart
48
+ let servers = [];
49
+ if (options.all) {
50
+ servers = registryService.listServers();
51
+ }
52
+ else if (options.tag) {
53
+ servers = registryService.listServers({ tag: options.tag });
54
+ }
55
+ else if (options.name) {
56
+ const server = registryService.findByName(options.name);
57
+ if (!server) {
58
+ throw new Error(`Server "${options.name}" not found`);
59
+ }
60
+ servers = [server];
61
+ }
62
+ else {
63
+ throw new Error("Either --name, --all, or --tag must be specified");
64
+ }
65
+ // Restart all matched servers
66
+ const results = [];
67
+ for (const server of servers) {
68
+ try {
69
+ let configRefreshed = false;
70
+ // Check if we should refresh config on restart (on-start mode)
71
+ if (config.refreshOnChange === "on-start") {
72
+ const drift = detectDrift(server, config);
73
+ if (drift.hasDrift) {
74
+ // Re-resolve command with new config values
75
+ const { resolvedCommand } = await refreshServerConfig(server, config, registryService);
76
+ configRefreshed = true;
77
+ // Delete old process and start with new command
78
+ try {
79
+ await processService.delete(server.pm2Name);
80
+ }
81
+ catch {
82
+ // Process might not exist
83
+ }
84
+ // Parse the resolved command to extract script and args
85
+ const parts = resolvedCommand.trim().split(/\s+/);
86
+ const script = parts[0] || "node";
87
+ const args = parts.slice(1);
88
+ // Get the updated server entry for current env
89
+ const updatedServer = registryService.findById(server.id);
90
+ const env = updatedServer?.env ?? server.env;
91
+ await processService.start({
92
+ name: server.pm2Name,
93
+ script,
94
+ args,
95
+ cwd: server.cwd,
96
+ env: {
97
+ ...env,
98
+ PORT: String(server.port),
99
+ },
100
+ });
101
+ }
102
+ else {
103
+ // No drift, just restart normally
104
+ await processService.restart(server.pm2Name);
105
+ }
106
+ }
107
+ else {
108
+ // Not in on-start mode, just restart normally
109
+ await processService.restart(server.pm2Name);
110
+ }
111
+ const status = await processService.getStatus(server.pm2Name);
112
+ results.push({
113
+ name: server.name,
114
+ success: true,
115
+ status,
116
+ configRefreshed,
117
+ });
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ results.push({
122
+ name: server.name,
123
+ success: false,
124
+ message,
125
+ });
126
+ }
127
+ }
128
+ // Return single result if single server was requested
129
+ if (options.name && results.length === 1) {
130
+ return results[0];
131
+ }
132
+ return results;
133
+ }
134
+ finally {
135
+ processService.disconnect();
136
+ }
137
+ }
138
+ /**
139
+ * CLI action handler for restart command
140
+ */
141
+ export async function restartAction(name, options) {
142
+ try {
143
+ if (!name && !options.all && !options.tag) {
144
+ if (options.json) {
145
+ console.log(formatErrorAsJson(new Error("Either server name, --all, or --tag must be specified")));
146
+ }
147
+ else {
148
+ console.error(formatError("Either server name, --all, or --tag must be specified"));
149
+ }
150
+ process.exitCode = 1;
151
+ return;
152
+ }
153
+ const result = await executeRestart({
154
+ name,
155
+ all: options.all,
156
+ tag: options.tag,
157
+ });
158
+ const results = Array.isArray(result) ? result : [result];
159
+ if (options.json) {
160
+ console.log(formatAsJson({ results }));
161
+ }
162
+ else {
163
+ console.log(formatRestartResult(results));
164
+ }
165
+ }
166
+ catch (error) {
167
+ if (options.json) {
168
+ console.log(formatErrorAsJson(error));
169
+ }
170
+ else {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ console.error(formatError(message));
173
+ }
174
+ logger.error({ error }, "Restart command failed");
175
+ process.exitCode = 1;
176
+ }
177
+ }
@@ -0,0 +1,37 @@
1
+ import type { ServerEntry, ServerStatus } from "../../types/registry.js";
2
+ export interface StartCommandOptions {
3
+ command: string;
4
+ cwd?: string;
5
+ name?: string;
6
+ port?: number;
7
+ protocol?: "http" | "https";
8
+ tags?: string[];
9
+ description?: string;
10
+ env?: Record<string, string>;
11
+ }
12
+ export interface StartCommandResult {
13
+ action: "started" | "existing" | "restarted" | "renamed";
14
+ server: ServerEntry;
15
+ status: ServerStatus;
16
+ portReassigned?: boolean;
17
+ originalPort?: number;
18
+ previousName?: string;
19
+ }
20
+ /**
21
+ * Execute the start command
22
+ */
23
+ export declare function executeStart(options: StartCommandOptions): Promise<StartCommandResult>;
24
+ /**
25
+ * CLI action handler for start command
26
+ */
27
+ export declare function startAction(commandArgs: string[], options: {
28
+ name?: string;
29
+ port?: number;
30
+ protocol?: "http" | "https";
31
+ tag?: string[];
32
+ description?: string;
33
+ env?: string[];
34
+ json?: boolean;
35
+ ci?: boolean;
36
+ noCi?: boolean;
37
+ }): Promise<void>;
@@ -0,0 +1,293 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import { ConfigService } from "../../services/config.service.js";
3
+ import { RegistryService } from "../../services/registry.service.js";
4
+ import { PortService } from "../../services/port.service.js";
5
+ import { ProcessService } from "../../services/process.service.js";
6
+ import { renderTemplate, parseEnvStrings, renderEnvTemplates, findMissingVariables, getTemplateVariables, formatMissingVariablesError, } from "../../utils/template.js";
7
+ import { extractUsedConfigKeys, createConfigSnapshot, } from "../../utils/config-drift.js";
8
+ import { formatStartResult } from "../output/formatters.js";
9
+ import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
10
+ import { logger } from "../../utils/logger.js";
11
+ import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
12
+ import { CIDetector } from "../../utils/ci-detector.js";
13
+ /**
14
+ * Execute the start command
15
+ */
16
+ export async function executeStart(options) {
17
+ const configService = new ConfigService();
18
+ const registryService = new RegistryService();
19
+ const processService = new ProcessService();
20
+ try {
21
+ // Load config and registry
22
+ const config = await configService.load();
23
+ await registryService.load();
24
+ // Connect to PM2
25
+ await processService.connect();
26
+ const cwd = options.cwd || process.cwd();
27
+ // Check if server already exists
28
+ const existingServer = registryService.findByCommandHash(cwd, options.command);
29
+ if (existingServer) {
30
+ // Check if user wants to rename the server
31
+ const shouldRename = options.name && options.name !== existingServer.name;
32
+ if (shouldRename) {
33
+ // Rename the server
34
+ const previousName = existingServer.name;
35
+ const newName = options.name;
36
+ const newPm2Name = `servherd-${newName}`;
37
+ // Delete the old PM2 process (if it exists)
38
+ try {
39
+ await processService.delete(existingServer.pm2Name);
40
+ }
41
+ catch {
42
+ // Process might not exist in PM2, that's okay
43
+ }
44
+ // Update the registry with new name
45
+ await registryService.updateServer(existingServer.id, {
46
+ name: newName,
47
+ pm2Name: newPm2Name,
48
+ });
49
+ const renamedServer = {
50
+ ...existingServer,
51
+ name: newName,
52
+ pm2Name: newPm2Name,
53
+ };
54
+ // Start the process with the new name
55
+ await startProcess(processService, renamedServer);
56
+ logger.info({ previousName, newName }, "Server renamed");
57
+ return {
58
+ action: "renamed",
59
+ server: renamedServer,
60
+ status: "online",
61
+ previousName,
62
+ };
63
+ }
64
+ // Server exists - check its status
65
+ const status = await processService.getStatus(existingServer.pm2Name);
66
+ if (status === "online") {
67
+ // Already running
68
+ return {
69
+ action: "existing",
70
+ server: existingServer,
71
+ status: "online",
72
+ };
73
+ }
74
+ // Stopped or errored - restart it
75
+ try {
76
+ await processService.restart(existingServer.pm2Name);
77
+ }
78
+ catch {
79
+ // Process might not exist in PM2, start it fresh
80
+ await startProcess(processService, existingServer);
81
+ }
82
+ return {
83
+ action: "restarted",
84
+ server: existingServer,
85
+ status: "online",
86
+ };
87
+ }
88
+ // New server - register and start
89
+ const portService = new PortService(config);
90
+ // Assign port with availability checking
91
+ const { port, reassigned: portReassigned } = await portService.assignPort(cwd, options.command, options.port);
92
+ // Track original port for reporting if reassigned
93
+ const originalPort = portReassigned
94
+ ? (options.port ?? portService.generatePort(cwd, options.command))
95
+ : undefined;
96
+ const hostname = config.hostname;
97
+ const protocol = options.protocol ?? config.protocol;
98
+ const url = `${protocol}://${hostname}:${port}`;
99
+ // Template variables for substitution (includes HTTPS cert/key paths)
100
+ const templateVars = {
101
+ port,
102
+ hostname,
103
+ url,
104
+ "https-cert": config.httpsCert ?? "",
105
+ "https-key": config.httpsKey ?? "",
106
+ };
107
+ // Resolve template variables in command
108
+ const resolvedCommand = renderTemplate(options.command, templateVars);
109
+ // Resolve template variables in environment values
110
+ const resolvedEnv = options.env
111
+ ? renderEnvTemplates(options.env, templateVars)
112
+ : undefined;
113
+ // Extract used config keys and create snapshot for drift detection
114
+ const usedConfigKeys = extractUsedConfigKeys(options.command);
115
+ const configSnapshot = createConfigSnapshot(config, usedConfigKeys);
116
+ // Register server
117
+ const server = await registryService.addServer({
118
+ command: options.command,
119
+ cwd,
120
+ port,
121
+ name: options.name,
122
+ protocol,
123
+ hostname,
124
+ tags: options.tags,
125
+ description: options.description,
126
+ env: resolvedEnv,
127
+ usedConfigKeys,
128
+ configSnapshot,
129
+ });
130
+ // Update with resolved command
131
+ await registryService.updateServer(server.id, {
132
+ resolvedCommand,
133
+ });
134
+ // Start the process
135
+ await startProcess(processService, {
136
+ ...server,
137
+ resolvedCommand,
138
+ });
139
+ return {
140
+ action: "started",
141
+ server: {
142
+ ...server,
143
+ resolvedCommand,
144
+ },
145
+ status: "online",
146
+ portReassigned,
147
+ originalPort,
148
+ };
149
+ }
150
+ finally {
151
+ processService.disconnect();
152
+ }
153
+ }
154
+ /**
155
+ * Start a process using PM2
156
+ */
157
+ async function startProcess(processService, server) {
158
+ // Parse the resolved command to extract script and args
159
+ const parts = parseCommand(server.resolvedCommand);
160
+ await processService.start({
161
+ name: server.pm2Name,
162
+ script: parts.script,
163
+ args: parts.args,
164
+ cwd: server.cwd,
165
+ env: {
166
+ ...server.env,
167
+ PORT: String(server.port),
168
+ },
169
+ });
170
+ }
171
+ /**
172
+ * Parse a command string into script and args
173
+ */
174
+ function parseCommand(command) {
175
+ const parts = command.trim().split(/\s+/);
176
+ const script = parts[0] || "node";
177
+ const args = parts.slice(1);
178
+ return { script, args };
179
+ }
180
+ /**
181
+ * Prompt user for missing template variables and update config
182
+ * @param missing - Array of missing variables
183
+ * @param configService - Config service instance
184
+ * @param config - Current config
185
+ * @returns Updated config with new values
186
+ */
187
+ async function promptForMissingVariables(missing, configService, config) {
188
+ const configurableMissing = missing.filter(v => v.configurable);
189
+ if (configurableMissing.length === 0) {
190
+ return config;
191
+ }
192
+ console.log("\nThe following template variables need to be configured:\n");
193
+ const updatedConfig = { ...config };
194
+ for (const v of configurableMissing) {
195
+ const value = await input({
196
+ message: v.prompt,
197
+ });
198
+ // Update config with the new value
199
+ if (v.configKey) {
200
+ // Handle nested config keys if needed
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ updatedConfig[v.configKey] = value;
203
+ }
204
+ }
205
+ // Save the updated config for future use
206
+ await configService.save(updatedConfig);
207
+ console.log("\n✓ Configuration saved for future use\n");
208
+ return updatedConfig;
209
+ }
210
+ /**
211
+ * CLI action handler for start command
212
+ */
213
+ export async function startAction(commandArgs, options) {
214
+ try {
215
+ const command = commandArgs.join(" ");
216
+ if (!command) {
217
+ const error = new ServherdError(ServherdErrorCode.COMMAND_MISSING_ARGUMENT, "Command is required");
218
+ if (options.json) {
219
+ console.log(formatErrorAsJson(error));
220
+ }
221
+ else {
222
+ console.error("Error: Command is required");
223
+ }
224
+ process.exitCode = 1;
225
+ return;
226
+ }
227
+ // Check for missing template variables before starting
228
+ const configService = new ConfigService();
229
+ let config = await configService.load();
230
+ // Use placeholder port to check config-based variables
231
+ const templateVars = getTemplateVariables(config, 0);
232
+ const missingVars = findMissingVariables(command, templateVars);
233
+ const configurableMissing = missingVars.filter(v => v.configurable);
234
+ // Check CI mode options
235
+ const ciModeOptions = {
236
+ ci: options.ci,
237
+ noCi: options.noCi,
238
+ };
239
+ const isCI = CIDetector.isCI(ciModeOptions);
240
+ if (configurableMissing.length > 0) {
241
+ if (isCI) {
242
+ // In CI mode, show error and exit
243
+ const errorMessage = formatMissingVariablesError(configurableMissing);
244
+ if (options.json) {
245
+ console.log(formatErrorAsJson(new ServherdError(ServherdErrorCode.CONFIG_VALIDATION_FAILED, errorMessage)));
246
+ }
247
+ else {
248
+ console.error(`Error: ${errorMessage}`);
249
+ }
250
+ process.exitCode = 1;
251
+ return;
252
+ }
253
+ // In interactive mode, prompt for missing values
254
+ config = await promptForMissingVariables(configurableMissing, configService, config);
255
+ }
256
+ // Parse environment variables from KEY=VALUE format
257
+ let env;
258
+ if (options.env && options.env.length > 0) {
259
+ env = parseEnvStrings(options.env);
260
+ }
261
+ const result = await executeStart({
262
+ command,
263
+ cwd: process.cwd(),
264
+ name: options.name,
265
+ port: options.port,
266
+ protocol: options.protocol,
267
+ tags: options.tag,
268
+ description: options.description,
269
+ env,
270
+ });
271
+ // Warn about port reassignment (unless in JSON mode)
272
+ if (result.portReassigned && !options.json) {
273
+ console.warn(`\x1b[33m⚠ Port ${result.originalPort} unavailable, reassigned to ${result.server.port}\x1b[0m`);
274
+ }
275
+ if (options.json) {
276
+ console.log(formatAsJson(result));
277
+ }
278
+ else {
279
+ console.log(formatStartResult(result));
280
+ }
281
+ }
282
+ catch (error) {
283
+ if (options.json) {
284
+ console.log(formatErrorAsJson(error));
285
+ }
286
+ else {
287
+ const message = error instanceof Error ? error.message : String(error);
288
+ console.error(`Error: ${message}`);
289
+ }
290
+ logger.error({ error }, "Start command failed");
291
+ process.exitCode = 1;
292
+ }
293
+ }
@@ -0,0 +1,20 @@
1
+ import { type StopResult } from "../output/formatters.js";
2
+ export interface StopCommandOptions {
3
+ name?: string;
4
+ all?: boolean;
5
+ tag?: string;
6
+ force?: boolean;
7
+ }
8
+ /**
9
+ * Execute the stop command
10
+ */
11
+ export declare function executeStop(options: StopCommandOptions): Promise<StopResult[]>;
12
+ /**
13
+ * CLI action handler for stop command
14
+ */
15
+ export declare function stopAction(name: string | undefined, options: {
16
+ all?: boolean;
17
+ tag?: string;
18
+ force?: boolean;
19
+ json?: boolean;
20
+ }): Promise<void>;
@@ -0,0 +1,108 @@
1
+ import { RegistryService } from "../../services/registry.service.js";
2
+ import { ProcessService } from "../../services/process.service.js";
3
+ import { formatStopResult } from "../output/formatters.js";
4
+ import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
5
+ import { logger } from "../../utils/logger.js";
6
+ /**
7
+ * Execute the stop command
8
+ */
9
+ export async function executeStop(options) {
10
+ const registryService = new RegistryService();
11
+ const processService = new ProcessService();
12
+ const results = [];
13
+ try {
14
+ await registryService.load();
15
+ await processService.connect();
16
+ let serversToStop = [];
17
+ if (options.all) {
18
+ serversToStop = registryService.listServers();
19
+ }
20
+ else if (options.tag) {
21
+ serversToStop = registryService.listServers({ tag: options.tag });
22
+ }
23
+ else if (options.name) {
24
+ const server = registryService.findByName(options.name);
25
+ if (server) {
26
+ serversToStop = [server];
27
+ }
28
+ else {
29
+ return [{
30
+ name: options.name,
31
+ success: false,
32
+ message: `Server "${options.name}" not found in registry`,
33
+ }];
34
+ }
35
+ }
36
+ for (const server of serversToStop) {
37
+ try {
38
+ // Use delete (SIGKILL) when force is specified, otherwise use stop (SIGTERM)
39
+ if (options.force) {
40
+ await processService.delete(server.pm2Name);
41
+ }
42
+ else {
43
+ await processService.stop(server.pm2Name);
44
+ }
45
+ results.push({
46
+ name: server.name,
47
+ success: true,
48
+ });
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ results.push({
53
+ name: server.name,
54
+ success: false,
55
+ message,
56
+ });
57
+ }
58
+ }
59
+ return results;
60
+ }
61
+ finally {
62
+ processService.disconnect();
63
+ }
64
+ }
65
+ /**
66
+ * CLI action handler for stop command
67
+ */
68
+ export async function stopAction(name, options) {
69
+ try {
70
+ if (!name && !options.all && !options.tag) {
71
+ if (options.json) {
72
+ console.log(formatErrorAsJson(new Error("Provide a server name, --all, or --tag")));
73
+ }
74
+ else {
75
+ console.error("Error: Provide a server name, --all, or --tag");
76
+ }
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ const results = await executeStop({
81
+ name,
82
+ all: options.all,
83
+ tag: options.tag,
84
+ force: options.force,
85
+ });
86
+ if (options.json) {
87
+ console.log(formatAsJson({ results }));
88
+ }
89
+ else {
90
+ console.log(formatStopResult(results));
91
+ }
92
+ // Set exit code if any failures
93
+ if (results.some((r) => !r.success)) {
94
+ process.exitCode = 1;
95
+ }
96
+ }
97
+ catch (error) {
98
+ if (options.json) {
99
+ console.log(formatErrorAsJson(error));
100
+ }
101
+ else {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ console.error(`Error: ${message}`);
104
+ }
105
+ logger.error({ error }, "Stop command failed");
106
+ process.exitCode = 1;
107
+ }
108
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ /**
3
+ * Create the CLI program
4
+ */
5
+ export declare function createProgram(): Command;
6
+ /**
7
+ * Run the CLI
8
+ */
9
+ export declare function runCLI(args?: string[]): Promise<void>;