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,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 {};