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,220 @@
|
|
|
1
|
+
import pm2 from "pm2";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
const SERVHERD_PREFIX = "servherd-";
|
|
4
|
+
/**
|
|
5
|
+
* Service for managing processes via PM2
|
|
6
|
+
*/
|
|
7
|
+
export class ProcessService {
|
|
8
|
+
connected = false;
|
|
9
|
+
/**
|
|
10
|
+
* Connect to PM2 daemon
|
|
11
|
+
*/
|
|
12
|
+
async connect() {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
pm2.connect((err) => {
|
|
15
|
+
if (err) {
|
|
16
|
+
logger.error({ error: err }, "Failed to connect to PM2");
|
|
17
|
+
reject(err);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.connected = true;
|
|
21
|
+
logger.debug("Connected to PM2");
|
|
22
|
+
resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Disconnect from PM2 daemon
|
|
28
|
+
*/
|
|
29
|
+
disconnect() {
|
|
30
|
+
pm2.disconnect();
|
|
31
|
+
this.connected = false;
|
|
32
|
+
logger.debug("Disconnected from PM2");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensure connected to PM2
|
|
36
|
+
*/
|
|
37
|
+
ensureConnected() {
|
|
38
|
+
if (!this.connected) {
|
|
39
|
+
throw new Error("Not connected to PM2. Call connect() first.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start a new process
|
|
44
|
+
*/
|
|
45
|
+
async start(options) {
|
|
46
|
+
this.ensureConnected();
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
pm2.start({
|
|
49
|
+
name: options.name,
|
|
50
|
+
script: options.script,
|
|
51
|
+
args: options.args,
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
env: options.env,
|
|
54
|
+
instances: options.instances,
|
|
55
|
+
autorestart: options.autorestart ?? false,
|
|
56
|
+
watch: options.watch ?? false,
|
|
57
|
+
max_memory_restart: options.max_memory_restart,
|
|
58
|
+
output: options.output,
|
|
59
|
+
error: options.error,
|
|
60
|
+
// Enable ISO timestamps in logs by default
|
|
61
|
+
log_date_format: options.log_date_format ?? "YYYY-MM-DDTHH:mm:ss.SSSZ",
|
|
62
|
+
}, (err, proc) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
logger.error({ error: err, name: options.name }, "Failed to start process");
|
|
65
|
+
reject(err);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
logger.info({ name: options.name }, "Process started");
|
|
69
|
+
resolve(proc);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Stop a process by name
|
|
75
|
+
*/
|
|
76
|
+
async stop(name) {
|
|
77
|
+
this.ensureConnected();
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
pm2.stop(name, (err, proc) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
logger.error({ error: err, name }, "Failed to stop process");
|
|
82
|
+
reject(err);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
logger.info({ name }, "Process stopped");
|
|
86
|
+
resolve(proc);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Restart a process by name
|
|
92
|
+
*/
|
|
93
|
+
async restart(name) {
|
|
94
|
+
this.ensureConnected();
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
pm2.restart(name, (err, proc) => {
|
|
97
|
+
if (err) {
|
|
98
|
+
logger.error({ error: err, name }, "Failed to restart process");
|
|
99
|
+
reject(err);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger.info({ name }, "Process restarted");
|
|
103
|
+
resolve(proc);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Delete a process by name
|
|
109
|
+
*/
|
|
110
|
+
async delete(name) {
|
|
111
|
+
this.ensureConnected();
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
pm2.delete(name, (err, proc) => {
|
|
114
|
+
if (err) {
|
|
115
|
+
logger.error({ error: err, name }, "Failed to delete process");
|
|
116
|
+
reject(err);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
logger.info({ name }, "Process deleted");
|
|
120
|
+
resolve(proc);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get process description
|
|
126
|
+
*/
|
|
127
|
+
async describe(name) {
|
|
128
|
+
this.ensureConnected();
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
pm2.describe(name, (err, procDesc) => {
|
|
131
|
+
if (err) {
|
|
132
|
+
logger.error({ error: err, name }, "Failed to describe process");
|
|
133
|
+
reject(err);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const desc = procDesc;
|
|
137
|
+
if (!desc || desc.length === 0) {
|
|
138
|
+
resolve(undefined);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve(desc[0]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* List all PM2 processes
|
|
147
|
+
*/
|
|
148
|
+
async list() {
|
|
149
|
+
this.ensureConnected();
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
pm2.list((err, procList) => {
|
|
152
|
+
if (err) {
|
|
153
|
+
logger.error({ error: err }, "Failed to list processes");
|
|
154
|
+
reject(err);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
resolve(procList || []);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* List only servherd-managed processes
|
|
163
|
+
*/
|
|
164
|
+
async listServherdProcesses() {
|
|
165
|
+
const allProcesses = await this.list();
|
|
166
|
+
return allProcesses.filter((p) => p.name.startsWith(SERVHERD_PREFIX));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the status of a process
|
|
170
|
+
*/
|
|
171
|
+
async getStatus(name) {
|
|
172
|
+
try {
|
|
173
|
+
const proc = await this.describe(name);
|
|
174
|
+
if (!proc) {
|
|
175
|
+
return "unknown";
|
|
176
|
+
}
|
|
177
|
+
const status = proc.pm2_env.status;
|
|
178
|
+
// Map PM2 status to ServerStatus
|
|
179
|
+
switch (status) {
|
|
180
|
+
case "online":
|
|
181
|
+
return "online";
|
|
182
|
+
case "stopped":
|
|
183
|
+
case "stopping":
|
|
184
|
+
return "stopped";
|
|
185
|
+
case "errored":
|
|
186
|
+
return "errored";
|
|
187
|
+
default:
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return "unknown";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if connected to PM2
|
|
197
|
+
*/
|
|
198
|
+
isConnected() {
|
|
199
|
+
return this.connected;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Flush (clear) logs for a process or all processes.
|
|
203
|
+
* @param name - Process name to flush logs for, or undefined/all for all processes
|
|
204
|
+
*/
|
|
205
|
+
async flush(name) {
|
|
206
|
+
this.ensureConnected();
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const pm2Name = name ?? "all";
|
|
209
|
+
pm2.flush(pm2Name, (err) => {
|
|
210
|
+
if (err) {
|
|
211
|
+
logger.error({ error: err, name: pm2Name }, "Failed to flush logs");
|
|
212
|
+
reject(err);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
logger.info({ name: pm2Name }, "Logs flushed");
|
|
216
|
+
resolve();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Registry, type ServerEntry, type ServerFilter, type AddServerOptions } from "../types/registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Service for managing the server registry
|
|
4
|
+
*/
|
|
5
|
+
export declare class RegistryService {
|
|
6
|
+
private registry;
|
|
7
|
+
private registryDir;
|
|
8
|
+
private registryPath;
|
|
9
|
+
constructor();
|
|
10
|
+
/**
|
|
11
|
+
* Load registry from file
|
|
12
|
+
*/
|
|
13
|
+
load(): Promise<Registry>;
|
|
14
|
+
/**
|
|
15
|
+
* Save registry to file
|
|
16
|
+
*/
|
|
17
|
+
save(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Add a new server to the registry
|
|
20
|
+
*/
|
|
21
|
+
addServer(options: AddServerOptions): Promise<ServerEntry>;
|
|
22
|
+
/**
|
|
23
|
+
* Find server by name
|
|
24
|
+
*/
|
|
25
|
+
findByName(name: string): ServerEntry | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Find server by ID
|
|
28
|
+
*/
|
|
29
|
+
findById(id: string): ServerEntry | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Find server by cwd and command (for detecting duplicate registrations)
|
|
32
|
+
*/
|
|
33
|
+
findByCommandHash(cwd: string, command: string): ServerEntry | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Update an existing server entry
|
|
36
|
+
*/
|
|
37
|
+
updateServer(id: string, updates: Partial<ServerEntry>): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Remove a server from the registry
|
|
40
|
+
*/
|
|
41
|
+
removeServer(id: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* List servers with optional filtering
|
|
44
|
+
*/
|
|
45
|
+
listServers(filter?: ServerFilter): ServerEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Get the registry file path
|
|
48
|
+
*/
|
|
49
|
+
getRegistryPath(): string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { pathExists, readJson, ensureDir, writeJson } from "fs-extra/esm";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as crypto from "crypto";
|
|
5
|
+
import micromatch from "micromatch";
|
|
6
|
+
import { RegistrySchema } from "../types/registry.js";
|
|
7
|
+
import { generateName } from "../utils/names.js";
|
|
8
|
+
import { logger } from "../utils/logger.js";
|
|
9
|
+
const DEFAULT_REGISTRY = {
|
|
10
|
+
version: "1",
|
|
11
|
+
servers: [],
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Service for managing the server registry
|
|
15
|
+
*/
|
|
16
|
+
export class RegistryService {
|
|
17
|
+
registry;
|
|
18
|
+
registryDir;
|
|
19
|
+
registryPath;
|
|
20
|
+
constructor() {
|
|
21
|
+
this.registry = { ...DEFAULT_REGISTRY, servers: [] };
|
|
22
|
+
this.registryDir = path.join(os.homedir(), ".servherd");
|
|
23
|
+
this.registryPath = path.join(this.registryDir, "registry.json");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load registry from file
|
|
27
|
+
*/
|
|
28
|
+
async load() {
|
|
29
|
+
try {
|
|
30
|
+
const exists = await pathExists(this.registryPath);
|
|
31
|
+
if (exists) {
|
|
32
|
+
const fileRegistry = await readJson(this.registryPath);
|
|
33
|
+
// Validate the loaded registry
|
|
34
|
+
const parsed = RegistrySchema.safeParse(fileRegistry);
|
|
35
|
+
if (parsed.success) {
|
|
36
|
+
this.registry = parsed.data;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logger.warn({ error: parsed.error }, "Invalid registry file, using empty registry");
|
|
40
|
+
this.registry = { ...DEFAULT_REGISTRY, servers: [] };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.registry = { ...DEFAULT_REGISTRY, servers: [] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
logger.warn("Failed to load registry file, using empty registry");
|
|
49
|
+
this.registry = { ...DEFAULT_REGISTRY, servers: [] };
|
|
50
|
+
}
|
|
51
|
+
return this.registry;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save registry to file
|
|
55
|
+
*/
|
|
56
|
+
async save() {
|
|
57
|
+
await ensureDir(this.registryDir);
|
|
58
|
+
await writeJson(this.registryPath, this.registry, { spaces: 2 });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Add a new server to the registry
|
|
62
|
+
*/
|
|
63
|
+
async addServer(options) {
|
|
64
|
+
const existingNames = new Set(this.registry.servers.map((s) => s.name));
|
|
65
|
+
const name = options.name || generateName(existingNames);
|
|
66
|
+
const id = crypto.randomUUID();
|
|
67
|
+
const entry = {
|
|
68
|
+
id,
|
|
69
|
+
name,
|
|
70
|
+
command: options.command,
|
|
71
|
+
resolvedCommand: options.command, // Will be resolved with actual port later
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
port: options.port,
|
|
74
|
+
protocol: options.protocol || "http",
|
|
75
|
+
hostname: options.hostname || "localhost",
|
|
76
|
+
env: options.env || {},
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
pm2Name: `servherd-${name}`,
|
|
79
|
+
tags: options.tags,
|
|
80
|
+
description: options.description,
|
|
81
|
+
usedConfigKeys: options.usedConfigKeys,
|
|
82
|
+
configSnapshot: options.configSnapshot,
|
|
83
|
+
};
|
|
84
|
+
this.registry.servers.push(entry);
|
|
85
|
+
await this.save();
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Find server by name
|
|
90
|
+
*/
|
|
91
|
+
findByName(name) {
|
|
92
|
+
return this.registry.servers.find((s) => s.name === name);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Find server by ID
|
|
96
|
+
*/
|
|
97
|
+
findById(id) {
|
|
98
|
+
return this.registry.servers.find((s) => s.id === id);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Find server by cwd and command (for detecting duplicate registrations)
|
|
102
|
+
*/
|
|
103
|
+
findByCommandHash(cwd, command) {
|
|
104
|
+
return this.registry.servers.find((s) => s.cwd === cwd && s.command === command);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Update an existing server entry
|
|
108
|
+
*/
|
|
109
|
+
async updateServer(id, updates) {
|
|
110
|
+
const index = this.registry.servers.findIndex((s) => s.id === id);
|
|
111
|
+
if (index === -1) {
|
|
112
|
+
throw new Error(`Server with id ${id} not found`);
|
|
113
|
+
}
|
|
114
|
+
this.registry.servers[index] = {
|
|
115
|
+
...this.registry.servers[index],
|
|
116
|
+
...updates,
|
|
117
|
+
};
|
|
118
|
+
await this.save();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Remove a server from the registry
|
|
122
|
+
*/
|
|
123
|
+
async removeServer(id) {
|
|
124
|
+
const index = this.registry.servers.findIndex((s) => s.id === id);
|
|
125
|
+
if (index === -1) {
|
|
126
|
+
throw new Error(`Server with id ${id} not found`);
|
|
127
|
+
}
|
|
128
|
+
this.registry.servers.splice(index, 1);
|
|
129
|
+
await this.save();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* List servers with optional filtering
|
|
133
|
+
*/
|
|
134
|
+
listServers(filter) {
|
|
135
|
+
let servers = [...this.registry.servers];
|
|
136
|
+
if (filter?.name) {
|
|
137
|
+
servers = servers.filter((s) => s.name === filter.name);
|
|
138
|
+
}
|
|
139
|
+
if (filter?.tag) {
|
|
140
|
+
servers = servers.filter((s) => s.tags?.includes(filter.tag));
|
|
141
|
+
}
|
|
142
|
+
if (filter?.cwd) {
|
|
143
|
+
servers = servers.filter((s) => s.cwd === filter.cwd);
|
|
144
|
+
}
|
|
145
|
+
if (filter?.cmd) {
|
|
146
|
+
const pattern = filter.cmd;
|
|
147
|
+
servers = servers.filter((s) => micromatch.isMatch(s.command, pattern));
|
|
148
|
+
}
|
|
149
|
+
return servers;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get the registry file path
|
|
153
|
+
*/
|
|
154
|
+
getRegistryPath() {
|
|
155
|
+
return this.registryPath;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const PortRangeSchema: z.ZodEffects<z.ZodObject<{
|
|
3
|
+
min: z.ZodNumber;
|
|
4
|
+
max: z.ZodNumber;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
}, {
|
|
9
|
+
min: number;
|
|
10
|
+
max: number;
|
|
11
|
+
}>, {
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
}, {
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const PM2ConfigSchema: z.ZodObject<{
|
|
19
|
+
logDir: z.ZodString;
|
|
20
|
+
pidDir: z.ZodString;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
logDir: string;
|
|
23
|
+
pidDir: string;
|
|
24
|
+
}, {
|
|
25
|
+
logDir: string;
|
|
26
|
+
pidDir: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Controls how servers are refreshed when config values change.
|
|
30
|
+
* - "manual": Requires explicit `servherd refresh` command
|
|
31
|
+
* - "prompt": Prompts user to restart affected servers when config changes via CLI
|
|
32
|
+
* - "auto": Automatically restarts affected servers when config changes via CLI
|
|
33
|
+
* - "on-start": Uses new config values on next start/restart (default, safest)
|
|
34
|
+
*/
|
|
35
|
+
export declare const RefreshOnChangeSchema: z.ZodEnum<["manual", "prompt", "auto", "on-start"]>;
|
|
36
|
+
export type RefreshOnChange = z.infer<typeof RefreshOnChangeSchema>;
|
|
37
|
+
export declare const GlobalConfigSchema: z.ZodObject<{
|
|
38
|
+
version: z.ZodString;
|
|
39
|
+
hostname: z.ZodString;
|
|
40
|
+
protocol: z.ZodEnum<["http", "https"]>;
|
|
41
|
+
portRange: z.ZodEffects<z.ZodObject<{
|
|
42
|
+
min: z.ZodNumber;
|
|
43
|
+
max: z.ZodNumber;
|
|
44
|
+
}, "strip", z.ZodTypeAny, {
|
|
45
|
+
min: number;
|
|
46
|
+
max: number;
|
|
47
|
+
}, {
|
|
48
|
+
min: number;
|
|
49
|
+
max: number;
|
|
50
|
+
}>, {
|
|
51
|
+
min: number;
|
|
52
|
+
max: number;
|
|
53
|
+
}, {
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
}>;
|
|
57
|
+
tempDir: z.ZodString;
|
|
58
|
+
pm2: z.ZodObject<{
|
|
59
|
+
logDir: z.ZodString;
|
|
60
|
+
pidDir: z.ZodString;
|
|
61
|
+
}, "strip", z.ZodTypeAny, {
|
|
62
|
+
logDir: string;
|
|
63
|
+
pidDir: string;
|
|
64
|
+
}, {
|
|
65
|
+
logDir: string;
|
|
66
|
+
pidDir: string;
|
|
67
|
+
}>;
|
|
68
|
+
httpsCert: z.ZodOptional<z.ZodString>;
|
|
69
|
+
httpsKey: z.ZodOptional<z.ZodString>;
|
|
70
|
+
refreshOnChange: z.ZodOptional<z.ZodEnum<["manual", "prompt", "auto", "on-start"]>>;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
version: string;
|
|
73
|
+
hostname: string;
|
|
74
|
+
protocol: "http" | "https";
|
|
75
|
+
portRange: {
|
|
76
|
+
min: number;
|
|
77
|
+
max: number;
|
|
78
|
+
};
|
|
79
|
+
tempDir: string;
|
|
80
|
+
pm2: {
|
|
81
|
+
logDir: string;
|
|
82
|
+
pidDir: string;
|
|
83
|
+
};
|
|
84
|
+
httpsCert?: string | undefined;
|
|
85
|
+
httpsKey?: string | undefined;
|
|
86
|
+
refreshOnChange?: "manual" | "prompt" | "auto" | "on-start" | undefined;
|
|
87
|
+
}, {
|
|
88
|
+
version: string;
|
|
89
|
+
hostname: string;
|
|
90
|
+
protocol: "http" | "https";
|
|
91
|
+
portRange: {
|
|
92
|
+
min: number;
|
|
93
|
+
max: number;
|
|
94
|
+
};
|
|
95
|
+
tempDir: string;
|
|
96
|
+
pm2: {
|
|
97
|
+
logDir: string;
|
|
98
|
+
pidDir: string;
|
|
99
|
+
};
|
|
100
|
+
httpsCert?: string | undefined;
|
|
101
|
+
httpsKey?: string | undefined;
|
|
102
|
+
refreshOnChange?: "manual" | "prompt" | "auto" | "on-start" | undefined;
|
|
103
|
+
}>;
|
|
104
|
+
export type PortRange = z.infer<typeof PortRangeSchema>;
|
|
105
|
+
export type PM2Config = z.infer<typeof PM2ConfigSchema>;
|
|
106
|
+
export type GlobalConfig = z.infer<typeof GlobalConfigSchema>;
|
|
107
|
+
export declare const DEFAULT_CONFIG: GlobalConfig;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const PortRangeSchema = z.object({
|
|
3
|
+
min: z.number().int().min(1).max(65535),
|
|
4
|
+
max: z.number().int().min(1).max(65535),
|
|
5
|
+
}).refine((data) => data.min <= data.max, {
|
|
6
|
+
message: "Port range min must be less than or equal to max",
|
|
7
|
+
});
|
|
8
|
+
export const PM2ConfigSchema = z.object({
|
|
9
|
+
logDir: z.string(),
|
|
10
|
+
pidDir: z.string(),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Controls how servers are refreshed when config values change.
|
|
14
|
+
* - "manual": Requires explicit `servherd refresh` command
|
|
15
|
+
* - "prompt": Prompts user to restart affected servers when config changes via CLI
|
|
16
|
+
* - "auto": Automatically restarts affected servers when config changes via CLI
|
|
17
|
+
* - "on-start": Uses new config values on next start/restart (default, safest)
|
|
18
|
+
*/
|
|
19
|
+
export const RefreshOnChangeSchema = z.enum(["manual", "prompt", "auto", "on-start"]);
|
|
20
|
+
export const GlobalConfigSchema = z.object({
|
|
21
|
+
version: z.string(),
|
|
22
|
+
hostname: z.string(),
|
|
23
|
+
protocol: z.enum(["http", "https"]),
|
|
24
|
+
portRange: PortRangeSchema,
|
|
25
|
+
tempDir: z.string(),
|
|
26
|
+
pm2: PM2ConfigSchema,
|
|
27
|
+
httpsCert: z.string().optional(),
|
|
28
|
+
httpsKey: z.string().optional(),
|
|
29
|
+
refreshOnChange: RefreshOnChangeSchema.optional(),
|
|
30
|
+
});
|
|
31
|
+
export const DEFAULT_CONFIG = {
|
|
32
|
+
version: "1",
|
|
33
|
+
hostname: "localhost",
|
|
34
|
+
protocol: "http",
|
|
35
|
+
portRange: { min: 3000, max: 9999 },
|
|
36
|
+
tempDir: "/tmp/servherd",
|
|
37
|
+
pm2: {
|
|
38
|
+
logDir: "/tmp/servherd/logs",
|
|
39
|
+
pidDir: "/tmp/servherd/pids",
|
|
40
|
+
},
|
|
41
|
+
httpsCert: undefined,
|
|
42
|
+
httpsKey: undefined,
|
|
43
|
+
refreshOnChange: "on-start",
|
|
44
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for servherd operations.
|
|
3
|
+
* Grouped by category:
|
|
4
|
+
* - 1xxx: Server-related errors
|
|
5
|
+
* - 2xxx: Port-related errors
|
|
6
|
+
* - 3xxx: PM2/Process-related errors
|
|
7
|
+
* - 4xxx: Configuration errors
|
|
8
|
+
* - 5xxx: Registry errors
|
|
9
|
+
* - 6xxx: Template errors
|
|
10
|
+
* - 7xxx: Command/CLI errors
|
|
11
|
+
* - 9xxx: Unknown/Other errors
|
|
12
|
+
*/
|
|
13
|
+
export declare enum ServherdErrorCode {
|
|
14
|
+
SERVER_NOT_FOUND = 1001,
|
|
15
|
+
SERVER_ALREADY_EXISTS = 1002,
|
|
16
|
+
SERVER_NOT_RUNNING = 1003,
|
|
17
|
+
SERVER_ALREADY_RUNNING = 1004,
|
|
18
|
+
PORT_UNAVAILABLE = 2001,
|
|
19
|
+
PORT_OUT_OF_RANGE = 2002,
|
|
20
|
+
PORT_ALLOCATION_FAILED = 2003,
|
|
21
|
+
PM2_CONNECTION_FAILED = 3001,
|
|
22
|
+
PM2_START_FAILED = 3002,
|
|
23
|
+
PM2_STOP_FAILED = 3003,
|
|
24
|
+
PM2_DELETE_FAILED = 3004,
|
|
25
|
+
PM2_RESTART_FAILED = 3005,
|
|
26
|
+
PM2_DESCRIBE_FAILED = 3006,
|
|
27
|
+
CONFIG_LOAD_FAILED = 4001,
|
|
28
|
+
CONFIG_SAVE_FAILED = 4002,
|
|
29
|
+
CONFIG_INVALID = 4003,
|
|
30
|
+
CONFIG_KEY_NOT_FOUND = 4004,
|
|
31
|
+
CONFIG_VALIDATION_FAILED = 4005,
|
|
32
|
+
REGISTRY_LOAD_FAILED = 5001,
|
|
33
|
+
REGISTRY_SAVE_FAILED = 5002,
|
|
34
|
+
REGISTRY_CORRUPT = 5003,
|
|
35
|
+
TEMPLATE_INVALID = 6001,
|
|
36
|
+
TEMPLATE_MISSING_VARIABLE = 6002,
|
|
37
|
+
COMMAND_INVALID = 7001,
|
|
38
|
+
COMMAND_MISSING_ARGUMENT = 7002,
|
|
39
|
+
COMMAND_CONFLICT = 7003,
|
|
40
|
+
INTERACTIVE_NOT_AVAILABLE = 7004,
|
|
41
|
+
UNKNOWN_ERROR = 9999
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Details that can be attached to a ServherdError for additional context.
|
|
45
|
+
*/
|
|
46
|
+
export interface ServherdErrorDetails {
|
|
47
|
+
exitCode?: number;
|
|
48
|
+
stderr?: string;
|
|
49
|
+
stdout?: string;
|
|
50
|
+
cause?: Error;
|
|
51
|
+
serverName?: string;
|
|
52
|
+
port?: number;
|
|
53
|
+
command?: string;
|
|
54
|
+
path?: string;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Custom error class for servherd operations.
|
|
59
|
+
* Includes error codes for programmatic handling and optional details.
|
|
60
|
+
*/
|
|
61
|
+
export declare class ServherdError extends Error {
|
|
62
|
+
readonly code: ServherdErrorCode;
|
|
63
|
+
readonly details?: ServherdErrorDetails;
|
|
64
|
+
constructor(code: ServherdErrorCode, message: string, details?: ServherdErrorDetails);
|
|
65
|
+
/**
|
|
66
|
+
* Serialize error to JSON for logging or API responses.
|
|
67
|
+
*/
|
|
68
|
+
toJSON(): Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Get the string name of the error code.
|
|
71
|
+
*/
|
|
72
|
+
getCodeName(): string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Type guard to check if an error is a ServherdError.
|
|
76
|
+
*/
|
|
77
|
+
export declare function isServherdError(error: unknown): error is ServherdError;
|
|
78
|
+
/**
|
|
79
|
+
* Format an error for CLI display.
|
|
80
|
+
* Includes color codes and proper formatting for terminal output.
|
|
81
|
+
*/
|
|
82
|
+
export declare function formatErrorForCLI(error: unknown): string;
|
|
83
|
+
/**
|
|
84
|
+
* MCP tool response content item.
|
|
85
|
+
*/
|
|
86
|
+
interface MCPContentItem {
|
|
87
|
+
type: "text";
|
|
88
|
+
text: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* MCP tool error response format.
|
|
92
|
+
*/
|
|
93
|
+
interface MCPErrorResponse {
|
|
94
|
+
isError: true;
|
|
95
|
+
content: MCPContentItem[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Format an error for MCP tool response.
|
|
99
|
+
* Returns a structured error response compatible with MCP protocol.
|
|
100
|
+
*/
|
|
101
|
+
export declare function formatErrorForMCP(error: unknown): MCPErrorResponse;
|
|
102
|
+
export {};
|