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,35 @@
|
|
|
1
|
+
import { type ConfigResult } from "../output/formatters.js";
|
|
2
|
+
export interface ConfigCommandOptions {
|
|
3
|
+
show?: boolean;
|
|
4
|
+
get?: string;
|
|
5
|
+
set?: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
reset?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
refresh?: string;
|
|
10
|
+
refreshAll?: boolean;
|
|
11
|
+
tag?: string;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Execute the config command
|
|
16
|
+
*/
|
|
17
|
+
export declare function executeConfig(options: ConfigCommandOptions): Promise<ConfigResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Run the interactive configuration wizard
|
|
20
|
+
* Prompts the user for all configuration values interactively
|
|
21
|
+
* @throws ServherdError if running in CI mode
|
|
22
|
+
*/
|
|
23
|
+
export declare function runConfigWizard(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* CLI action handler for config command
|
|
26
|
+
*/
|
|
27
|
+
export declare function configAction(options: {
|
|
28
|
+
show?: boolean;
|
|
29
|
+
get?: string;
|
|
30
|
+
set?: string;
|
|
31
|
+
value?: string;
|
|
32
|
+
reset?: boolean;
|
|
33
|
+
force?: boolean;
|
|
34
|
+
json?: boolean;
|
|
35
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
2
|
+
import { ConfigService } from "../../services/config.service.js";
|
|
3
|
+
import { RegistryService } from "../../services/registry.service.js";
|
|
4
|
+
import { formatConfigResult } from "../output/formatters.js";
|
|
5
|
+
import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
7
|
+
import { CIDetector } from "../../utils/ci-detector.js";
|
|
8
|
+
import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
|
|
9
|
+
import { findServersUsingConfigKey } from "../../utils/config-drift.js";
|
|
10
|
+
import { executeRefresh } from "./refresh.js";
|
|
11
|
+
// Valid top-level config keys
|
|
12
|
+
const VALID_TOP_LEVEL_KEYS = ["version", "hostname", "protocol", "portRange", "tempDir", "pm2", "httpsCert", "httpsKey", "refreshOnChange"];
|
|
13
|
+
const VALID_NESTED_KEYS = ["portRange.min", "portRange.max", "pm2.logDir", "pm2.pidDir"];
|
|
14
|
+
// Config keys that can affect server commands (used for drift detection)
|
|
15
|
+
const SERVER_AFFECTING_KEYS = ["hostname", "httpsCert", "httpsKey"];
|
|
16
|
+
function isValidKey(key) {
|
|
17
|
+
return VALID_TOP_LEVEL_KEYS.includes(key) || VALID_NESTED_KEYS.includes(key);
|
|
18
|
+
}
|
|
19
|
+
function getNestedValue(config, key) {
|
|
20
|
+
const parts = key.split(".");
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
let current = config;
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
if (current && typeof current === "object" && part in current) {
|
|
25
|
+
current = current[part];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
function setNestedValue(config, key, value) {
|
|
34
|
+
const parts = key.split(".");
|
|
35
|
+
const result = { ...config };
|
|
36
|
+
if (parts.length === 1) {
|
|
37
|
+
// Top-level key
|
|
38
|
+
result[key] = value;
|
|
39
|
+
}
|
|
40
|
+
else if (parts.length === 2) {
|
|
41
|
+
// Nested key (e.g., portRange.min)
|
|
42
|
+
const [parentKey, childKey] = parts;
|
|
43
|
+
if (parentKey === "portRange") {
|
|
44
|
+
result.portRange = { ...result.portRange, [childKey]: value };
|
|
45
|
+
}
|
|
46
|
+
else if (parentKey === "pm2") {
|
|
47
|
+
result.pm2 = { ...result.pm2, [childKey]: value };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Handle server refresh after a config change based on refreshOnChange setting
|
|
54
|
+
*/
|
|
55
|
+
async function handleConfigChangeRefresh(config, changedKey) {
|
|
56
|
+
// Only handle server-affecting keys
|
|
57
|
+
if (!SERVER_AFFECTING_KEYS.includes(changedKey)) {
|
|
58
|
+
return { refreshed: false };
|
|
59
|
+
}
|
|
60
|
+
const refreshOnChange = config.refreshOnChange ?? "on-start";
|
|
61
|
+
// manual and on-start modes don't auto-refresh
|
|
62
|
+
if (refreshOnChange === "manual" || refreshOnChange === "on-start") {
|
|
63
|
+
return { refreshed: false };
|
|
64
|
+
}
|
|
65
|
+
// Find servers that use this config key
|
|
66
|
+
const registryService = new RegistryService();
|
|
67
|
+
await registryService.load();
|
|
68
|
+
const allServers = registryService.listServers();
|
|
69
|
+
const affectedServers = findServersUsingConfigKey(allServers, changedKey);
|
|
70
|
+
if (affectedServers.length === 0) {
|
|
71
|
+
return { refreshed: false };
|
|
72
|
+
}
|
|
73
|
+
const serverNames = affectedServers.map(s => s.name).join(", ");
|
|
74
|
+
if (refreshOnChange === "prompt") {
|
|
75
|
+
// Don't prompt in CI mode
|
|
76
|
+
if (CIDetector.isCI()) {
|
|
77
|
+
return {
|
|
78
|
+
refreshed: false,
|
|
79
|
+
message: `${affectedServers.length} server(s) use this config value. Run "servherd refresh" to apply changes.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Prompt user
|
|
83
|
+
const shouldRefresh = await confirm({
|
|
84
|
+
message: `${affectedServers.length} server(s) use this config value (${serverNames}). Restart them now?`,
|
|
85
|
+
});
|
|
86
|
+
if (!shouldRefresh) {
|
|
87
|
+
return {
|
|
88
|
+
refreshed: false,
|
|
89
|
+
message: "Run \"servherd refresh\" later to apply changes to affected servers.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Execute refresh for all servers (will filter to those with drift)
|
|
94
|
+
try {
|
|
95
|
+
await executeRefresh({ all: true });
|
|
96
|
+
return {
|
|
97
|
+
refreshed: true,
|
|
98
|
+
message: `Refreshed ${affectedServers.length} server(s): ${serverNames}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
103
|
+
return {
|
|
104
|
+
refreshed: false,
|
|
105
|
+
message: `Failed to refresh servers: ${errorMsg}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Execute the config command
|
|
111
|
+
*/
|
|
112
|
+
export async function executeConfig(options) {
|
|
113
|
+
const configService = new ConfigService();
|
|
114
|
+
// Load current config
|
|
115
|
+
await configService.load();
|
|
116
|
+
// Handle --refresh or --refresh-all
|
|
117
|
+
if (options.refresh || options.refreshAll) {
|
|
118
|
+
const refreshResults = await executeRefresh({
|
|
119
|
+
name: options.refresh,
|
|
120
|
+
all: options.refreshAll,
|
|
121
|
+
tag: options.tag,
|
|
122
|
+
dryRun: options.dryRun,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
refreshResults,
|
|
126
|
+
dryRun: options.dryRun,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Handle --get
|
|
130
|
+
if (options.get) {
|
|
131
|
+
const key = options.get;
|
|
132
|
+
if (!isValidKey(key)) {
|
|
133
|
+
return {
|
|
134
|
+
error: `Unknown configuration key: "${key}"`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const config = await configService.load();
|
|
138
|
+
const value = getNestedValue(config, key);
|
|
139
|
+
return {
|
|
140
|
+
key,
|
|
141
|
+
value,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Handle --set
|
|
145
|
+
if (options.set) {
|
|
146
|
+
const key = options.set;
|
|
147
|
+
if (options.value === undefined) {
|
|
148
|
+
return {
|
|
149
|
+
updated: false,
|
|
150
|
+
error: "--value is required when using --set",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (!isValidKey(key)) {
|
|
154
|
+
return {
|
|
155
|
+
updated: false,
|
|
156
|
+
error: `Unknown configuration key: "${key}"`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Validate and convert value based on key
|
|
160
|
+
let parsedValue = options.value;
|
|
161
|
+
if (key === "protocol") {
|
|
162
|
+
if (options.value !== "http" && options.value !== "https") {
|
|
163
|
+
return {
|
|
164
|
+
updated: false,
|
|
165
|
+
error: "Invalid protocol value. Must be \"http\" or \"https\"",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else if (key === "refreshOnChange") {
|
|
170
|
+
const validModes = ["manual", "prompt", "auto", "on-start"];
|
|
171
|
+
if (!validModes.includes(options.value)) {
|
|
172
|
+
return {
|
|
173
|
+
updated: false,
|
|
174
|
+
error: `Invalid refreshOnChange value. Must be one of: ${validModes.join(", ")}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (key === "portRange.min" || key === "portRange.max") {
|
|
179
|
+
const num = parseInt(options.value, 10);
|
|
180
|
+
if (isNaN(num)) {
|
|
181
|
+
return {
|
|
182
|
+
updated: false,
|
|
183
|
+
error: "Invalid port value. Must be a number",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (num < 1 || num > 65535) {
|
|
187
|
+
return {
|
|
188
|
+
updated: false,
|
|
189
|
+
error: "Invalid port value. Must be between 1 and 65535",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
parsedValue = num;
|
|
193
|
+
}
|
|
194
|
+
// Handle nested keys vs top-level keys
|
|
195
|
+
let updatedConfig;
|
|
196
|
+
if (key.includes(".")) {
|
|
197
|
+
const config = await configService.load();
|
|
198
|
+
updatedConfig = setNestedValue(config, key, parsedValue);
|
|
199
|
+
await configService.save(updatedConfig);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
await configService.set(key, parsedValue);
|
|
203
|
+
updatedConfig = await configService.load();
|
|
204
|
+
}
|
|
205
|
+
// Handle refresh behavior after config change
|
|
206
|
+
const refreshResult = await handleConfigChangeRefresh(updatedConfig, key);
|
|
207
|
+
return {
|
|
208
|
+
updated: true,
|
|
209
|
+
key,
|
|
210
|
+
value: parsedValue,
|
|
211
|
+
refreshMessage: refreshResult.message,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Handle --reset
|
|
215
|
+
if (options.reset) {
|
|
216
|
+
if (!options.force) {
|
|
217
|
+
const confirmed = await confirm({
|
|
218
|
+
message: "Are you sure you want to reset configuration to defaults?",
|
|
219
|
+
});
|
|
220
|
+
if (!confirmed) {
|
|
221
|
+
return {
|
|
222
|
+
reset: false,
|
|
223
|
+
cancelled: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const defaults = configService.getDefaults();
|
|
228
|
+
await configService.save(defaults);
|
|
229
|
+
return {
|
|
230
|
+
reset: true,
|
|
231
|
+
config: defaults,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Handle --show (default)
|
|
235
|
+
const config = await configService.load();
|
|
236
|
+
const configPath = configService.getLoadedConfigPath();
|
|
237
|
+
const globalConfigPath = configService.getConfigPath();
|
|
238
|
+
return {
|
|
239
|
+
config,
|
|
240
|
+
configPath,
|
|
241
|
+
globalConfigPath,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Run the interactive configuration wizard
|
|
246
|
+
* Prompts the user for all configuration values interactively
|
|
247
|
+
* @throws ServherdError if running in CI mode
|
|
248
|
+
*/
|
|
249
|
+
export async function runConfigWizard() {
|
|
250
|
+
if (CIDetector.isCI()) {
|
|
251
|
+
throw new ServherdError(ServherdErrorCode.INTERACTIVE_NOT_AVAILABLE, "Interactive config not available in CI mode. Use \"servherd config --set <key> --value <value>\"");
|
|
252
|
+
}
|
|
253
|
+
const configService = new ConfigService();
|
|
254
|
+
await configService.load();
|
|
255
|
+
const currentConfig = await configService.load();
|
|
256
|
+
// Prompt for hostname
|
|
257
|
+
const hostname = await input({
|
|
258
|
+
message: "Default hostname:",
|
|
259
|
+
default: currentConfig.hostname,
|
|
260
|
+
});
|
|
261
|
+
// Prompt for protocol
|
|
262
|
+
const protocol = await select({
|
|
263
|
+
message: "Default protocol:",
|
|
264
|
+
choices: [
|
|
265
|
+
{ name: "HTTP", value: "http" },
|
|
266
|
+
{ name: "HTTPS", value: "https" },
|
|
267
|
+
],
|
|
268
|
+
default: currentConfig.protocol,
|
|
269
|
+
});
|
|
270
|
+
// If HTTPS, prompt for cert and key paths
|
|
271
|
+
let httpsCert;
|
|
272
|
+
let httpsKey;
|
|
273
|
+
if (protocol === "https") {
|
|
274
|
+
httpsCert = await input({
|
|
275
|
+
message: "Path to HTTPS certificate:",
|
|
276
|
+
default: currentConfig.httpsCert,
|
|
277
|
+
});
|
|
278
|
+
httpsKey = await input({
|
|
279
|
+
message: "Path to HTTPS key:",
|
|
280
|
+
default: currentConfig.httpsKey,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Prompt for port range
|
|
284
|
+
const portMinStr = await input({
|
|
285
|
+
message: "Minimum port:",
|
|
286
|
+
default: String(currentConfig.portRange.min),
|
|
287
|
+
validate: (v) => !isNaN(parseInt(v)) || "Must be a number",
|
|
288
|
+
});
|
|
289
|
+
const portMaxStr = await input({
|
|
290
|
+
message: "Maximum port:",
|
|
291
|
+
default: String(currentConfig.portRange.max),
|
|
292
|
+
validate: (v) => !isNaN(parseInt(v)) || "Must be a number",
|
|
293
|
+
});
|
|
294
|
+
// Save the configuration
|
|
295
|
+
const newConfig = {
|
|
296
|
+
...currentConfig,
|
|
297
|
+
hostname,
|
|
298
|
+
protocol,
|
|
299
|
+
httpsCert,
|
|
300
|
+
httpsKey,
|
|
301
|
+
portRange: {
|
|
302
|
+
min: parseInt(portMinStr, 10),
|
|
303
|
+
max: parseInt(portMaxStr, 10),
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
await configService.save(newConfig);
|
|
307
|
+
console.log("✓ Configuration saved");
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* CLI action handler for config command
|
|
311
|
+
*/
|
|
312
|
+
export async function configAction(options) {
|
|
313
|
+
try {
|
|
314
|
+
const result = await executeConfig(options);
|
|
315
|
+
if (options.json) {
|
|
316
|
+
console.log(formatAsJson(result));
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
console.log(formatConfigResult(result));
|
|
320
|
+
}
|
|
321
|
+
if (result.error) {
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (options.json) {
|
|
327
|
+
console.log(formatErrorAsJson(error));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
331
|
+
console.error(`Error: ${message}`);
|
|
332
|
+
}
|
|
333
|
+
logger.error({ error }, "Config command failed");
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ServerStatus } from "../../types/registry.js";
|
|
2
|
+
export interface InfoCommandOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface InfoCommandResult {
|
|
6
|
+
name: string;
|
|
7
|
+
status: ServerStatus;
|
|
8
|
+
url: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
command: string;
|
|
11
|
+
resolvedCommand: string;
|
|
12
|
+
port: number;
|
|
13
|
+
hostname: string;
|
|
14
|
+
protocol: string;
|
|
15
|
+
pid?: number;
|
|
16
|
+
uptime?: number;
|
|
17
|
+
restarts?: number;
|
|
18
|
+
memory?: number;
|
|
19
|
+
cpu?: number;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
description?: string;
|
|
22
|
+
env?: Record<string, string>;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
pm2Name: string;
|
|
25
|
+
outLogPath?: string;
|
|
26
|
+
errLogPath?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Execute the info command
|
|
30
|
+
*/
|
|
31
|
+
export declare function executeInfo(options: InfoCommandOptions): Promise<InfoCommandResult>;
|
|
32
|
+
/**
|
|
33
|
+
* CLI action handler for info command
|
|
34
|
+
*/
|
|
35
|
+
export declare function infoAction(name: string, options?: {
|
|
36
|
+
json?: boolean;
|
|
37
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { RegistryService } from "../../services/registry.service.js";
|
|
2
|
+
import { ProcessService } from "../../services/process.service.js";
|
|
3
|
+
import { formatServerInfo } from "../output/formatters.js";
|
|
4
|
+
import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Execute the info command
|
|
8
|
+
*/
|
|
9
|
+
export async function executeInfo(options) {
|
|
10
|
+
const registryService = new RegistryService();
|
|
11
|
+
const processService = new ProcessService();
|
|
12
|
+
try {
|
|
13
|
+
// Load registry
|
|
14
|
+
await registryService.load();
|
|
15
|
+
// Find server by name
|
|
16
|
+
const server = registryService.findByName(options.name);
|
|
17
|
+
if (!server) {
|
|
18
|
+
throw new Error(`Server "${options.name}" not found`);
|
|
19
|
+
}
|
|
20
|
+
// Connect to PM2 to get process details
|
|
21
|
+
await processService.connect();
|
|
22
|
+
// Get process info from PM2
|
|
23
|
+
const procDesc = await processService.describe(server.pm2Name);
|
|
24
|
+
// Build result
|
|
25
|
+
const result = {
|
|
26
|
+
name: server.name,
|
|
27
|
+
status: "unknown",
|
|
28
|
+
url: `${server.protocol}://${server.hostname}:${server.port}`,
|
|
29
|
+
cwd: server.cwd,
|
|
30
|
+
command: server.command,
|
|
31
|
+
resolvedCommand: server.resolvedCommand,
|
|
32
|
+
port: server.port,
|
|
33
|
+
hostname: server.hostname,
|
|
34
|
+
protocol: server.protocol,
|
|
35
|
+
tags: server.tags,
|
|
36
|
+
description: server.description,
|
|
37
|
+
env: server.env,
|
|
38
|
+
createdAt: server.createdAt,
|
|
39
|
+
pm2Name: server.pm2Name,
|
|
40
|
+
};
|
|
41
|
+
if (procDesc) {
|
|
42
|
+
// Process exists in PM2
|
|
43
|
+
const pm2Env = procDesc.pm2_env;
|
|
44
|
+
result.status = pm2Env.status === "online" ? "online"
|
|
45
|
+
: pm2Env.status === "stopped" || pm2Env.status === "stopping" ? "stopped"
|
|
46
|
+
: pm2Env.status === "errored" ? "errored"
|
|
47
|
+
: "unknown";
|
|
48
|
+
result.pid = procDesc.pid;
|
|
49
|
+
result.uptime = pm2Env.pm_uptime;
|
|
50
|
+
result.restarts = pm2Env.restart_time;
|
|
51
|
+
result.outLogPath = pm2Env.pm_out_log_path;
|
|
52
|
+
result.errLogPath = pm2Env.pm_err_log_path;
|
|
53
|
+
if (procDesc.monit) {
|
|
54
|
+
result.memory = procDesc.monit.memory;
|
|
55
|
+
result.cpu = procDesc.monit.cpu;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
processService.disconnect();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* CLI action handler for info command
|
|
66
|
+
*/
|
|
67
|
+
export async function infoAction(name, options) {
|
|
68
|
+
try {
|
|
69
|
+
if (!name) {
|
|
70
|
+
if (options?.json) {
|
|
71
|
+
console.log(formatErrorAsJson(new Error("Server name is required")));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.error("Error: Server name is required");
|
|
75
|
+
}
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const result = await executeInfo({ name });
|
|
80
|
+
if (options?.json) {
|
|
81
|
+
console.log(formatAsJson(result));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(formatServerInfo(result));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (options?.json) {
|
|
89
|
+
console.log(formatErrorAsJson(error));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
console.error(`Error: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
logger.error({ error }, "Info command failed");
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ServerListItem } from "../output/formatters.js";
|
|
2
|
+
export interface ListCommandOptions {
|
|
3
|
+
running?: boolean;
|
|
4
|
+
stopped?: boolean;
|
|
5
|
+
tag?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
cmd?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ListCommandResult {
|
|
10
|
+
servers: ServerListItem[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Execute the list command
|
|
14
|
+
*/
|
|
15
|
+
export declare function executeList(options: ListCommandOptions): Promise<ListCommandResult>;
|
|
16
|
+
/**
|
|
17
|
+
* CLI action handler for list command
|
|
18
|
+
*/
|
|
19
|
+
export declare function listAction(options: {
|
|
20
|
+
running?: boolean;
|
|
21
|
+
stopped?: boolean;
|
|
22
|
+
tag?: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
cmd?: string;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,86 @@
|
|
|
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 { formatServerListTable } from "../output/formatters.js";
|
|
5
|
+
import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
7
|
+
import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
|
|
8
|
+
import { detectDrift } from "../../utils/config-drift.js";
|
|
9
|
+
/**
|
|
10
|
+
* Execute the list command
|
|
11
|
+
*/
|
|
12
|
+
export async function executeList(options) {
|
|
13
|
+
// Validate mutually exclusive options
|
|
14
|
+
if (options.running && options.stopped) {
|
|
15
|
+
throw new ServherdError(ServherdErrorCode.COMMAND_CONFLICT, "Cannot specify both --running and --stopped");
|
|
16
|
+
}
|
|
17
|
+
const registryService = new RegistryService();
|
|
18
|
+
const processService = new ProcessService();
|
|
19
|
+
const configService = new ConfigService();
|
|
20
|
+
try {
|
|
21
|
+
await registryService.load();
|
|
22
|
+
await processService.connect();
|
|
23
|
+
const config = await configService.load();
|
|
24
|
+
// Get servers from registry with optional filters
|
|
25
|
+
const servers = registryService.listServers({
|
|
26
|
+
tag: options.tag,
|
|
27
|
+
cwd: options.cwd,
|
|
28
|
+
cmd: options.cmd,
|
|
29
|
+
});
|
|
30
|
+
// Get status for each server
|
|
31
|
+
const serverListItems = [];
|
|
32
|
+
for (const server of servers) {
|
|
33
|
+
const status = await processService.getStatus(server.pm2Name);
|
|
34
|
+
// Filter by running status if requested
|
|
35
|
+
if (options.running && status !== "online") {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Filter by stopped status if requested
|
|
39
|
+
if (options.stopped && status !== "stopped") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Detect config drift
|
|
43
|
+
const drift = detectDrift(server, config);
|
|
44
|
+
serverListItems.push({
|
|
45
|
+
server,
|
|
46
|
+
status,
|
|
47
|
+
hasDrift: drift.hasDrift,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { servers: serverListItems };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
processService.disconnect();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* CLI action handler for list command
|
|
58
|
+
*/
|
|
59
|
+
export async function listAction(options) {
|
|
60
|
+
try {
|
|
61
|
+
const result = await executeList({
|
|
62
|
+
running: options.running,
|
|
63
|
+
stopped: options.stopped,
|
|
64
|
+
tag: options.tag,
|
|
65
|
+
cwd: options.cwd,
|
|
66
|
+
cmd: options.cmd,
|
|
67
|
+
});
|
|
68
|
+
if (options.json) {
|
|
69
|
+
console.log(formatAsJson(result));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(formatServerListTable(result.servers));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (options.json) {
|
|
77
|
+
console.log(formatErrorAsJson(error));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
console.error(`Error: ${message}`);
|
|
82
|
+
}
|
|
83
|
+
logger.error({ error }, "List command failed");
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ServerStatus } from "../../types/registry.js";
|
|
2
|
+
export interface LogsCommandOptions {
|
|
3
|
+
name?: string;
|
|
4
|
+
lines?: number;
|
|
5
|
+
error?: boolean;
|
|
6
|
+
follow?: boolean;
|
|
7
|
+
since?: string;
|
|
8
|
+
head?: number;
|
|
9
|
+
flush?: boolean;
|
|
10
|
+
all?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface LogsCommandResult {
|
|
13
|
+
name: string;
|
|
14
|
+
status: ServerStatus;
|
|
15
|
+
logs: string;
|
|
16
|
+
lines: number;
|
|
17
|
+
outLogPath?: string;
|
|
18
|
+
errLogPath?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface FlushCommandResult {
|
|
21
|
+
flushed: boolean;
|
|
22
|
+
name?: string;
|
|
23
|
+
all?: boolean;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute flush command to clear logs
|
|
28
|
+
*/
|
|
29
|
+
export declare function executeFlush(options: LogsCommandOptions): Promise<FlushCommandResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Execute the logs command
|
|
32
|
+
*/
|
|
33
|
+
export declare function executeLogs(options: LogsCommandOptions): Promise<LogsCommandResult>;
|
|
34
|
+
/**
|
|
35
|
+
* CLI action handler for logs command
|
|
36
|
+
*/
|
|
37
|
+
export declare function logsAction(name: string | undefined, options: {
|
|
38
|
+
lines?: number;
|
|
39
|
+
error?: boolean;
|
|
40
|
+
follow?: boolean;
|
|
41
|
+
since?: string;
|
|
42
|
+
head?: number;
|
|
43
|
+
flush?: boolean;
|
|
44
|
+
all?: boolean;
|
|
45
|
+
json?: boolean;
|
|
46
|
+
}): Promise<void>;
|