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,292 @@
1
+ import { readFile } from "fs/promises";
2
+ import { pathExists } from "fs-extra/esm";
3
+ import { RegistryService } from "../../services/registry.service.js";
4
+ import { ProcessService } from "../../services/process.service.js";
5
+ import { formatLogs, formatError, formatWarning, formatSuccess } from "../output/formatters.js";
6
+ import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
7
+ import { logger } from "../../utils/logger.js";
8
+ import { followLog } from "../../utils/log-follower.js";
9
+ import { parseTimeFilter, filterLogsByTime } from "../../utils/time-parser.js";
10
+ import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
11
+ const DEFAULT_LINES = 50;
12
+ /**
13
+ * Parse timestamp from PM2-style log lines.
14
+ * PM2 outputs timestamps in ISO 8601 format: 2026-01-11T09:51:49.598-08:00: message
15
+ * Returns null if no timestamp is found.
16
+ */
17
+ function parseLogTimestamp(line) {
18
+ // PM2 format: "2026-01-11T09:51:49.598-08:00: message"
19
+ // Extract everything before the first ": " as potential timestamp
20
+ const colonIndex = line.indexOf(": ");
21
+ if (colonIndex === -1) {
22
+ return null;
23
+ }
24
+ const potentialTimestamp = line.substring(0, colonIndex);
25
+ const date = new Date(potentialTimestamp);
26
+ if (!isNaN(date.getTime())) {
27
+ return date;
28
+ }
29
+ return null;
30
+ }
31
+ /**
32
+ * Execute flush command to clear logs
33
+ */
34
+ export async function executeFlush(options) {
35
+ const registryService = new RegistryService();
36
+ const processService = new ProcessService();
37
+ try {
38
+ await processService.connect();
39
+ if (options.all) {
40
+ await processService.flush();
41
+ return {
42
+ flushed: true,
43
+ all: true,
44
+ message: "Logs flushed for all servers",
45
+ };
46
+ }
47
+ if (!options.name) {
48
+ throw new ServherdError(ServherdErrorCode.COMMAND_MISSING_ARGUMENT, "Server name is required (or use --all to flush all logs)");
49
+ }
50
+ // Load registry to verify server exists
51
+ await registryService.load();
52
+ const server = registryService.findByName(options.name);
53
+ if (!server) {
54
+ throw new ServherdError(ServherdErrorCode.SERVER_NOT_FOUND, `Server "${options.name}" not found`);
55
+ }
56
+ await processService.flush(server.pm2Name);
57
+ return {
58
+ flushed: true,
59
+ name: options.name,
60
+ message: `Logs flushed for server "${options.name}"`,
61
+ };
62
+ }
63
+ finally {
64
+ processService.disconnect();
65
+ }
66
+ }
67
+ /**
68
+ * Execute the logs command
69
+ */
70
+ export async function executeLogs(options) {
71
+ const registryService = new RegistryService();
72
+ const processService = new ProcessService();
73
+ if (!options.name) {
74
+ throw new ServherdError(ServherdErrorCode.COMMAND_MISSING_ARGUMENT, "Server name is required");
75
+ }
76
+ try {
77
+ // Load registry
78
+ await registryService.load();
79
+ // Find server by name
80
+ const server = registryService.findByName(options.name);
81
+ if (!server) {
82
+ throw new ServherdError(ServherdErrorCode.SERVER_NOT_FOUND, `Server "${options.name}" not found`);
83
+ }
84
+ // Connect to PM2 to get process details
85
+ await processService.connect();
86
+ // Get process info from PM2
87
+ const procDesc = await processService.describe(server.pm2Name);
88
+ const lines = options.lines ?? DEFAULT_LINES;
89
+ let outLogPath;
90
+ let errLogPath;
91
+ let status = "unknown";
92
+ let logs = "";
93
+ if (procDesc) {
94
+ const pm2Env = procDesc.pm2_env;
95
+ outLogPath = pm2Env.pm_out_log_path;
96
+ errLogPath = pm2Env.pm_err_log_path;
97
+ status = pm2Env.status === "online" ? "online"
98
+ : pm2Env.status === "stopped" || pm2Env.status === "stopping" ? "stopped"
99
+ : pm2Env.status === "errored" ? "errored"
100
+ : "unknown";
101
+ // Read the appropriate log file
102
+ const logPath = options.error ? errLogPath : outLogPath;
103
+ if (logPath) {
104
+ try {
105
+ const exists = await pathExists(logPath);
106
+ if (exists) {
107
+ const content = await readFile(logPath, "utf-8");
108
+ // Filter out empty strings caused by trailing newlines
109
+ let allLines = content.split("\n").filter((line) => line.length > 0);
110
+ // Apply --since filter if specified
111
+ if (options.since) {
112
+ const sinceDate = parseTimeFilter(options.since);
113
+ allLines = filterLogsByTime(allLines, sinceDate, parseLogTimestamp);
114
+ }
115
+ // Apply --head or --lines
116
+ if (options.head !== undefined) {
117
+ // --head shows first N lines
118
+ const headLines = allLines.slice(0, options.head);
119
+ logs = headLines.join("\n");
120
+ }
121
+ else {
122
+ // Default: --lines shows last N lines
123
+ const lastLines = allLines.slice(-lines);
124
+ logs = lastLines.join("\n");
125
+ }
126
+ }
127
+ else {
128
+ logs = "(log file does not exist)";
129
+ }
130
+ }
131
+ catch (error) {
132
+ logger.debug({ error, logPath }, "Failed to read log file");
133
+ logs = "(failed to read log file)";
134
+ }
135
+ }
136
+ else {
137
+ logs = "(no log path available)";
138
+ }
139
+ }
140
+ else {
141
+ logs = "(process not found in PM2)";
142
+ }
143
+ return {
144
+ name: options.name,
145
+ status,
146
+ logs,
147
+ lines: options.head ?? lines,
148
+ outLogPath,
149
+ errLogPath,
150
+ };
151
+ }
152
+ finally {
153
+ processService.disconnect();
154
+ }
155
+ }
156
+ /**
157
+ * CLI action handler for logs command
158
+ */
159
+ export async function logsAction(name, options) {
160
+ try {
161
+ // Handle --flush option
162
+ if (options.flush) {
163
+ const result = await executeFlush({ name, all: options.all });
164
+ if (options.json) {
165
+ console.log(formatAsJson(result));
166
+ }
167
+ else {
168
+ console.log(formatSuccess(result.message));
169
+ }
170
+ return;
171
+ }
172
+ // Validate that name is provided for non-flush operations
173
+ if (!name) {
174
+ const error = new ServherdError(ServherdErrorCode.COMMAND_MISSING_ARGUMENT, "Server name is required");
175
+ if (options.json) {
176
+ console.log(formatErrorAsJson(error));
177
+ }
178
+ else {
179
+ console.error(formatError(error.message));
180
+ }
181
+ process.exitCode = 1;
182
+ return;
183
+ }
184
+ // Handle --follow option
185
+ if (options.follow && !options.json) {
186
+ await handleFollowMode(name, options);
187
+ return;
188
+ }
189
+ if (options.follow && options.json) {
190
+ // Follow mode doesn't work well with JSON output
191
+ console.log(formatWarning("The --follow flag is not supported with --json output. Showing static logs."));
192
+ }
193
+ const result = await executeLogs({
194
+ name,
195
+ lines: options.lines,
196
+ error: options.error,
197
+ follow: options.follow,
198
+ since: options.since,
199
+ head: options.head,
200
+ });
201
+ if (options.json) {
202
+ console.log(formatAsJson(result));
203
+ }
204
+ else {
205
+ console.log(formatLogs(result));
206
+ }
207
+ }
208
+ catch (error) {
209
+ if (options.json) {
210
+ console.log(formatErrorAsJson(error));
211
+ }
212
+ else {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ console.error(formatError(message));
215
+ }
216
+ logger.error({ error }, "Logs command failed");
217
+ process.exitCode = 1;
218
+ }
219
+ }
220
+ /**
221
+ * Handle follow mode for logs
222
+ */
223
+ async function handleFollowMode(name, options) {
224
+ const registryService = new RegistryService();
225
+ const processService = new ProcessService();
226
+ try {
227
+ await registryService.load();
228
+ const server = registryService.findByName(name);
229
+ if (!server) {
230
+ console.error(formatError(`Server "${name}" not found`));
231
+ process.exitCode = 1;
232
+ return;
233
+ }
234
+ await processService.connect();
235
+ const procDesc = await processService.describe(server.pm2Name);
236
+ if (!procDesc) {
237
+ console.error(formatError(`Process not found in PM2 for "${name}"`));
238
+ process.exitCode = 1;
239
+ return;
240
+ }
241
+ const logPath = options.error
242
+ ? procDesc.pm2_env.pm_err_log_path
243
+ : procDesc.pm2_env.pm_out_log_path;
244
+ if (!logPath) {
245
+ console.error(formatError("No log path available"));
246
+ process.exitCode = 1;
247
+ return;
248
+ }
249
+ const exists = await pathExists(logPath);
250
+ if (!exists) {
251
+ console.error(formatError(`Log file does not exist: ${logPath}`));
252
+ process.exitCode = 1;
253
+ return;
254
+ }
255
+ console.log(formatWarning(`Following logs for "${name}" (Ctrl+C to stop)...`));
256
+ console.log("");
257
+ // Set up abort controller for graceful shutdown
258
+ const controller = new AbortController();
259
+ // Handle SIGINT (Ctrl+C)
260
+ const handleSignal = () => {
261
+ controller.abort();
262
+ };
263
+ process.on("SIGINT", handleSignal);
264
+ process.on("SIGTERM", handleSignal);
265
+ // Optional: parse --since for filtering live logs
266
+ let sinceDate;
267
+ if (options.since) {
268
+ sinceDate = parseTimeFilter(options.since);
269
+ }
270
+ try {
271
+ await followLog(logPath, controller.signal, (line) => {
272
+ // Filter by time if --since is specified
273
+ if (sinceDate) {
274
+ const timestamp = parseLogTimestamp(line);
275
+ if (timestamp && timestamp < sinceDate) {
276
+ return; // Skip old lines
277
+ }
278
+ }
279
+ console.log(line);
280
+ });
281
+ }
282
+ finally {
283
+ process.off("SIGINT", handleSignal);
284
+ process.off("SIGTERM", handleSignal);
285
+ }
286
+ console.log("");
287
+ console.log(formatSuccess("Stopped following logs."));
288
+ }
289
+ finally {
290
+ processService.disconnect();
291
+ }
292
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * CLI action handler for mcp command
3
+ * Starts the MCP server in stdio mode for use with Claude Code or other MCP clients
4
+ */
5
+ export declare function mcpAction(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { startStdioServer } from "../../mcp/index.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ /**
4
+ * CLI action handler for mcp command
5
+ * Starts the MCP server in stdio mode for use with Claude Code or other MCP clients
6
+ */
7
+ export async function mcpAction() {
8
+ try {
9
+ await startStdioServer();
10
+ }
11
+ catch (error) {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ // Don't use console.error as it would interfere with stdio transport
14
+ logger.error({ error }, "MCP server failed: " + message);
15
+ process.exitCode = 1;
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ import type { ServerStatus } from "../../types/registry.js";
2
+ export interface RefreshCommandOptions {
3
+ name?: string;
4
+ all?: boolean;
5
+ tag?: string;
6
+ dryRun?: boolean;
7
+ }
8
+ export interface RefreshResult {
9
+ name: string;
10
+ success: boolean;
11
+ status?: ServerStatus;
12
+ message?: string;
13
+ driftDetails?: string;
14
+ skipped?: boolean;
15
+ }
16
+ /**
17
+ * Execute the refresh command
18
+ * Finds servers with config drift and restarts them with updated config
19
+ */
20
+ export declare function executeRefresh(options: RefreshCommandOptions): Promise<RefreshResult[]>;
@@ -0,0 +1,139 @@
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 { renderTemplate, renderEnvTemplates, getTemplateVariables } from "../../utils/template.js";
5
+ import { extractUsedConfigKeys, createConfigSnapshot, findServersWithDrift, formatDrift, } from "../../utils/config-drift.js";
6
+ /**
7
+ * Re-resolve a server's command template with current config values
8
+ * Updates the registry with new resolved command and config snapshot
9
+ */
10
+ async function refreshServerConfig(server, config, registryService) {
11
+ // Get template variables with current config
12
+ const templateVars = getTemplateVariables(config, server.port);
13
+ // Re-resolve the command template
14
+ const resolvedCommand = renderTemplate(server.command, templateVars);
15
+ // Re-resolve environment variables if any
16
+ const resolvedEnv = server.env
17
+ ? renderEnvTemplates(server.env, templateVars)
18
+ : {};
19
+ // Extract new used config keys and create new snapshot
20
+ const usedConfigKeys = extractUsedConfigKeys(server.command);
21
+ const configSnapshot = createConfigSnapshot(config, usedConfigKeys);
22
+ // Update the registry
23
+ await registryService.updateServer(server.id, {
24
+ resolvedCommand,
25
+ env: resolvedEnv,
26
+ usedConfigKeys,
27
+ configSnapshot,
28
+ });
29
+ return { resolvedCommand };
30
+ }
31
+ /**
32
+ * Execute the refresh command
33
+ * Finds servers with config drift and restarts them with updated config
34
+ */
35
+ export async function executeRefresh(options) {
36
+ const registryService = new RegistryService();
37
+ const processService = new ProcessService();
38
+ const configService = new ConfigService();
39
+ try {
40
+ // Load registry and config
41
+ await registryService.load();
42
+ const config = await configService.load();
43
+ // Connect to PM2
44
+ await processService.connect();
45
+ // Determine which servers to check
46
+ let servers = [];
47
+ if (options.all) {
48
+ servers = registryService.listServers();
49
+ }
50
+ else if (options.tag) {
51
+ servers = registryService.listServers({ tag: options.tag });
52
+ }
53
+ else if (options.name) {
54
+ const server = registryService.findByName(options.name);
55
+ if (!server) {
56
+ throw new Error(`Server "${options.name}" not found`);
57
+ }
58
+ servers = [server];
59
+ }
60
+ else {
61
+ // Default: find all servers with drift
62
+ servers = registryService.listServers();
63
+ }
64
+ // Find servers with drift
65
+ const serversWithDrift = findServersWithDrift(servers, config);
66
+ // If no drift, return early
67
+ if (serversWithDrift.length === 0) {
68
+ return [{
69
+ name: "",
70
+ success: true,
71
+ skipped: true,
72
+ message: "No servers have config drift",
73
+ }];
74
+ }
75
+ const results = [];
76
+ for (const { server, drift } of serversWithDrift) {
77
+ const driftDetails = formatDrift(drift);
78
+ // Dry run mode - just report what would happen
79
+ if (options.dryRun) {
80
+ results.push({
81
+ name: server.name,
82
+ success: true,
83
+ skipped: true,
84
+ message: "Would refresh (dry-run mode)",
85
+ driftDetails,
86
+ });
87
+ continue;
88
+ }
89
+ try {
90
+ // Re-resolve command with new config values
91
+ const { resolvedCommand } = await refreshServerConfig(server, config, registryService);
92
+ // Delete old process and start with new command
93
+ try {
94
+ await processService.delete(server.pm2Name);
95
+ }
96
+ catch {
97
+ // Process might not exist
98
+ }
99
+ // Parse the resolved command to extract script and args
100
+ const parts = resolvedCommand.trim().split(/\s+/);
101
+ const script = parts[0] || "node";
102
+ const args = parts.slice(1);
103
+ // Get the updated server entry for current env
104
+ const updatedServer = registryService.findById(server.id);
105
+ const env = updatedServer?.env ?? server.env;
106
+ await processService.start({
107
+ name: server.pm2Name,
108
+ script,
109
+ args,
110
+ cwd: server.cwd,
111
+ env: {
112
+ ...env,
113
+ PORT: String(server.port),
114
+ },
115
+ });
116
+ const status = await processService.getStatus(server.pm2Name);
117
+ results.push({
118
+ name: server.name,
119
+ success: true,
120
+ status,
121
+ driftDetails,
122
+ });
123
+ }
124
+ catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ results.push({
127
+ name: server.name,
128
+ success: false,
129
+ message,
130
+ driftDetails,
131
+ });
132
+ }
133
+ }
134
+ return results;
135
+ }
136
+ finally {
137
+ processService.disconnect();
138
+ }
139
+ }
@@ -0,0 +1,20 @@
1
+ import { type RemoveResult } from "../output/formatters.js";
2
+ export interface RemoveCommandOptions {
3
+ name?: string;
4
+ all?: boolean;
5
+ tag?: string;
6
+ force?: boolean;
7
+ }
8
+ /**
9
+ * Execute the remove command
10
+ */
11
+ export declare function executeRemove(options: RemoveCommandOptions): Promise<RemoveResult[]>;
12
+ /**
13
+ * CLI action handler for remove command
14
+ */
15
+ export declare function removeAction(name: string | undefined, options: {
16
+ all?: boolean;
17
+ tag?: string;
18
+ force?: boolean;
19
+ json?: boolean;
20
+ }): Promise<void>;
@@ -0,0 +1,144 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+ import { RegistryService } from "../../services/registry.service.js";
3
+ import { ProcessService } from "../../services/process.service.js";
4
+ import { formatRemoveResult } from "../output/formatters.js";
5
+ import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ /**
8
+ * Execute the remove command
9
+ */
10
+ export async function executeRemove(options) {
11
+ const registryService = new RegistryService();
12
+ const processService = new ProcessService();
13
+ const results = [];
14
+ try {
15
+ await registryService.load();
16
+ await processService.connect();
17
+ let serversToRemove = [];
18
+ if (options.all) {
19
+ serversToRemove = registryService.listServers();
20
+ }
21
+ else if (options.tag) {
22
+ serversToRemove = registryService.listServers({ tag: options.tag });
23
+ }
24
+ else if (options.name) {
25
+ const server = registryService.findByName(options.name);
26
+ if (server) {
27
+ serversToRemove = [server];
28
+ }
29
+ else {
30
+ return [{
31
+ name: options.name,
32
+ success: false,
33
+ message: `Server "${options.name}" not found in registry`,
34
+ }];
35
+ }
36
+ }
37
+ if (serversToRemove.length === 0) {
38
+ return [];
39
+ }
40
+ // Ask for confirmation unless --force is specified
41
+ if (!options.force) {
42
+ const serverNames = serversToRemove.map((s) => s.name).join(", ");
43
+ const message = serversToRemove.length === 1
44
+ ? `Are you sure you want to remove server "${serversToRemove[0].name}"?`
45
+ : `Are you sure you want to remove ${serversToRemove.length} servers (${serverNames})?`;
46
+ const confirmed = await confirm({ message });
47
+ if (!confirmed) {
48
+ return serversToRemove.map((server) => ({
49
+ name: server.name,
50
+ success: false,
51
+ cancelled: true,
52
+ message: "Cancelled by user",
53
+ }));
54
+ }
55
+ }
56
+ for (const server of serversToRemove) {
57
+ try {
58
+ // First try to delete from PM2
59
+ let pm2DeleteFailed = false;
60
+ try {
61
+ await processService.delete(server.pm2Name);
62
+ }
63
+ catch (error) {
64
+ // If PM2 delete fails due to process not found, we still want to remove from registry
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ if (!message.includes("not found") && !message.includes("process name not found")) {
67
+ pm2DeleteFailed = true;
68
+ results.push({
69
+ name: server.name,
70
+ success: false,
71
+ message,
72
+ });
73
+ continue;
74
+ }
75
+ // Process not found in PM2 is okay - we'll still remove from registry
76
+ }
77
+ // Then remove from registry
78
+ if (!pm2DeleteFailed) {
79
+ await registryService.removeServer(server.id);
80
+ results.push({
81
+ name: server.name,
82
+ success: true,
83
+ });
84
+ }
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ results.push({
89
+ name: server.name,
90
+ success: false,
91
+ message,
92
+ });
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ finally {
98
+ processService.disconnect();
99
+ }
100
+ }
101
+ /**
102
+ * CLI action handler for remove command
103
+ */
104
+ export async function removeAction(name, options) {
105
+ try {
106
+ if (!name && !options.all && !options.tag) {
107
+ if (options.json) {
108
+ console.log(formatErrorAsJson(new Error("Provide a server name, --all, or --tag")));
109
+ }
110
+ else {
111
+ console.error("Error: Provide a server name, --all, or --tag");
112
+ }
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+ const results = await executeRemove({
117
+ name,
118
+ all: options.all,
119
+ tag: options.tag,
120
+ force: options.force,
121
+ });
122
+ if (options.json) {
123
+ console.log(formatAsJson({ results }));
124
+ }
125
+ else {
126
+ console.log(formatRemoveResult(results));
127
+ }
128
+ // Set exit code if any failures (excluding cancellations)
129
+ if (results.some((r) => !r.success && !r.cancelled)) {
130
+ process.exitCode = 1;
131
+ }
132
+ }
133
+ catch (error) {
134
+ if (options.json) {
135
+ console.log(formatErrorAsJson(error));
136
+ }
137
+ else {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ console.error(`Error: ${message}`);
140
+ }
141
+ logger.error({ error }, "Remove command failed");
142
+ process.exitCode = 1;
143
+ }
144
+ }
@@ -0,0 +1,25 @@
1
+ import type { ServerStatus } from "../../types/registry.js";
2
+ export interface RestartCommandOptions {
3
+ name?: string;
4
+ all?: boolean;
5
+ tag?: string;
6
+ }
7
+ export interface RestartResult {
8
+ name: string;
9
+ success: boolean;
10
+ status?: ServerStatus;
11
+ message?: string;
12
+ configRefreshed?: boolean;
13
+ }
14
+ /**
15
+ * Execute the restart command for a single server
16
+ */
17
+ export declare function executeRestart(options: RestartCommandOptions): Promise<RestartResult | RestartResult[]>;
18
+ /**
19
+ * CLI action handler for restart command
20
+ */
21
+ export declare function restartAction(name: string | undefined, options: {
22
+ all?: boolean;
23
+ tag?: string;
24
+ json?: boolean;
25
+ }): Promise<void>;