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.
- package/CONTRIBUTING.md +250 -0
- package/LICENSE +21 -0
- package/README.md +653 -29
- package/dist/cli/commands/config.d.ts +35 -0
- package/dist/cli/commands/config.js +336 -0
- package/dist/cli/commands/info.d.ts +37 -0
- package/dist/cli/commands/info.js +98 -0
- package/dist/cli/commands/list.d.ts +26 -0
- package/dist/cli/commands/list.js +86 -0
- package/dist/cli/commands/logs.d.ts +46 -0
- package/dist/cli/commands/logs.js +292 -0
- package/dist/cli/commands/mcp.d.ts +5 -0
- package/dist/cli/commands/mcp.js +17 -0
- package/dist/cli/commands/refresh.d.ts +20 -0
- package/dist/cli/commands/refresh.js +139 -0
- package/dist/cli/commands/remove.d.ts +20 -0
- package/dist/cli/commands/remove.js +144 -0
- package/dist/cli/commands/restart.d.ts +25 -0
- package/dist/cli/commands/restart.js +177 -0
- package/dist/cli/commands/start.d.ts +37 -0
- package/dist/cli/commands/start.js +293 -0
- package/dist/cli/commands/stop.d.ts +20 -0
- package/dist/cli/commands/stop.js +108 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +160 -0
- package/dist/cli/output/formatters.d.ts +117 -0
- package/dist/cli/output/formatters.js +454 -0
- package/dist/cli/output/json-formatter.d.ts +22 -0
- package/dist/cli/output/json-formatter.js +40 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +25 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.js +352 -0
- package/dist/mcp/resources/servers.d.ts +14 -0
- package/dist/mcp/resources/servers.js +128 -0
- package/dist/mcp/tools/config.d.ts +33 -0
- package/dist/mcp/tools/config.js +88 -0
- package/dist/mcp/tools/info.d.ts +36 -0
- package/dist/mcp/tools/info.js +65 -0
- package/dist/mcp/tools/list.d.ts +36 -0
- package/dist/mcp/tools/list.js +49 -0
- package/dist/mcp/tools/logs.d.ts +44 -0
- package/dist/mcp/tools/logs.js +55 -0
- package/dist/mcp/tools/refresh.d.ts +33 -0
- package/dist/mcp/tools/refresh.js +54 -0
- package/dist/mcp/tools/remove.d.ts +23 -0
- package/dist/mcp/tools/remove.js +43 -0
- package/dist/mcp/tools/restart.d.ts +23 -0
- package/dist/mcp/tools/restart.js +42 -0
- package/dist/mcp/tools/start.d.ts +38 -0
- package/dist/mcp/tools/start.js +73 -0
- package/dist/mcp/tools/stop.d.ts +23 -0
- package/dist/mcp/tools/stop.js +40 -0
- package/dist/services/config.service.d.ts +80 -0
- package/dist/services/config.service.js +227 -0
- package/dist/services/port.service.d.ts +82 -0
- package/dist/services/port.service.js +151 -0
- package/dist/services/process.service.d.ts +61 -0
- package/dist/services/process.service.js +220 -0
- package/dist/services/registry.service.d.ts +50 -0
- package/dist/services/registry.service.js +157 -0
- package/dist/types/config.d.ts +107 -0
- package/dist/types/config.js +44 -0
- package/dist/types/errors.d.ts +102 -0
- package/dist/types/errors.js +197 -0
- package/dist/types/pm2.d.ts +50 -0
- package/dist/types/pm2.js +4 -0
- package/dist/types/registry.d.ts +230 -0
- package/dist/types/registry.js +33 -0
- package/dist/utils/ci-detector.d.ts +31 -0
- package/dist/utils/ci-detector.js +68 -0
- package/dist/utils/config-drift.d.ts +71 -0
- package/dist/utils/config-drift.js +128 -0
- package/dist/utils/error-handler.d.ts +21 -0
- package/dist/utils/error-handler.js +38 -0
- package/dist/utils/log-follower.d.ts +10 -0
- package/dist/utils/log-follower.js +98 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/names.d.ts +7 -0
- package/dist/utils/names.js +20 -0
- package/dist/utils/template.d.ts +88 -0
- package/dist/utils/template.js +180 -0
- package/dist/utils/time-parser.d.ts +19 -0
- package/dist/utils/time-parser.js +54 -0
- package/docs/ci-cd.md +408 -0
- package/docs/configuration.md +325 -0
- package/docs/mcp-integration.md +411 -0
- package/examples/basic-usage/README.md +187 -0
- package/examples/ci-github-actions/workflow.yml +195 -0
- package/examples/mcp-claude-code/README.md +213 -0
- package/examples/multi-server/README.md +270 -0
- package/examples/storybook/README.md +187 -0
- package/examples/vite-project/README.md +251 -0
- 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
|
+
}
|