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,80 @@
1
+ import { type GlobalConfig } from "../types/config.js";
2
+ /**
3
+ * Configuration search locations (in order of precedence):
4
+ * 1. Project-local configs (searched from cwd upward):
5
+ * - package.json "servherd" key
6
+ * - .servherdrc (JSON or YAML)
7
+ * - .servherdrc.json
8
+ * - .servherdrc.yaml / .servherdrc.yml
9
+ * - .servherdrc.js / .servherdrc.cjs
10
+ * - servherd.config.js / servherd.config.cjs
11
+ * 2. Global config: ~/.servherd/config.json
12
+ * 3. Environment variables (highest priority)
13
+ */
14
+ /**
15
+ * Service for managing global configuration using cosmiconfig
16
+ */
17
+ export declare class ConfigService {
18
+ private config;
19
+ private globalConfigDir;
20
+ private globalConfigPath;
21
+ private loadedFilepath;
22
+ private explorer;
23
+ constructor();
24
+ /**
25
+ * Load configuration from multiple sources with the following priority:
26
+ * 1. Environment variables (highest)
27
+ * 2. Project-local config file (if found)
28
+ * 3. Global config file (~/.servherd/config.json)
29
+ * 4. Default values (lowest)
30
+ */
31
+ load(searchFrom?: string): Promise<GlobalConfig>;
32
+ /**
33
+ * Load the global config file from ~/.servherd/config.json
34
+ */
35
+ private loadGlobalConfig;
36
+ /**
37
+ * Search for project-local config starting from the given directory
38
+ */
39
+ private searchProjectConfig;
40
+ /**
41
+ * Merge partial config into base config (supports deeply partial configs)
42
+ */
43
+ private mergeConfigs;
44
+ /**
45
+ * Save configuration to the global config file
46
+ */
47
+ save(config: GlobalConfig): Promise<void>;
48
+ /**
49
+ * Get a configuration value
50
+ */
51
+ get<K extends keyof GlobalConfig>(key: K): GlobalConfig[K];
52
+ /**
53
+ * Set a configuration value and persist to the global config file
54
+ */
55
+ set<K extends keyof GlobalConfig>(key: K, value: GlobalConfig[K]): Promise<void>;
56
+ /**
57
+ * Get default configuration
58
+ */
59
+ getDefaults(): GlobalConfig;
60
+ /**
61
+ * Get the global config file path
62
+ */
63
+ getConfigPath(): string;
64
+ /**
65
+ * Get the path of the currently loaded config file (if any)
66
+ */
67
+ getLoadedConfigPath(): string | null;
68
+ /**
69
+ * Get all supported config file names for documentation
70
+ */
71
+ getSupportedConfigFiles(): string[];
72
+ /**
73
+ * Apply environment variable overrides to configuration
74
+ */
75
+ applyEnvironmentOverrides(config: GlobalConfig): GlobalConfig;
76
+ /**
77
+ * Clear the cosmiconfig cache (useful for testing or after config changes)
78
+ */
79
+ clearCache(): void;
80
+ }
@@ -0,0 +1,227 @@
1
+ import { cosmiconfig } from "cosmiconfig";
2
+ import { ensureDir, writeJson } from "fs-extra/esm";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { DEFAULT_CONFIG, GlobalConfigSchema } from "../types/config.js";
6
+ import { logger } from "../utils/logger.js";
7
+ const MODULE_NAME = "servherd";
8
+ /**
9
+ * Configuration search locations (in order of precedence):
10
+ * 1. Project-local configs (searched from cwd upward):
11
+ * - package.json "servherd" key
12
+ * - .servherdrc (JSON or YAML)
13
+ * - .servherdrc.json
14
+ * - .servherdrc.yaml / .servherdrc.yml
15
+ * - .servherdrc.js / .servherdrc.cjs
16
+ * - servherd.config.js / servherd.config.cjs
17
+ * 2. Global config: ~/.servherd/config.json
18
+ * 3. Environment variables (highest priority)
19
+ */
20
+ /**
21
+ * Service for managing global configuration using cosmiconfig
22
+ */
23
+ export class ConfigService {
24
+ config;
25
+ globalConfigDir;
26
+ globalConfigPath;
27
+ loadedFilepath = null;
28
+ explorer = cosmiconfig(MODULE_NAME, {
29
+ searchPlaces: [
30
+ "package.json",
31
+ `.${MODULE_NAME}rc`,
32
+ `.${MODULE_NAME}rc.json`,
33
+ `.${MODULE_NAME}rc.yaml`,
34
+ `.${MODULE_NAME}rc.yml`,
35
+ `.${MODULE_NAME}rc.js`,
36
+ `.${MODULE_NAME}rc.cjs`,
37
+ `${MODULE_NAME}.config.js`,
38
+ `${MODULE_NAME}.config.cjs`,
39
+ ],
40
+ });
41
+ constructor() {
42
+ this.config = { ...DEFAULT_CONFIG };
43
+ this.globalConfigDir = path.join(os.homedir(), ".servherd");
44
+ this.globalConfigPath = path.join(this.globalConfigDir, "config.json");
45
+ }
46
+ /**
47
+ * Load configuration from multiple sources with the following priority:
48
+ * 1. Environment variables (highest)
49
+ * 2. Project-local config file (if found)
50
+ * 3. Global config file (~/.servherd/config.json)
51
+ * 4. Default values (lowest)
52
+ */
53
+ async load(searchFrom) {
54
+ let baseConfig = { ...DEFAULT_CONFIG };
55
+ // First, try to load global config
56
+ const globalResult = await this.loadGlobalConfig();
57
+ if (globalResult) {
58
+ baseConfig = this.mergeConfigs(baseConfig, globalResult);
59
+ }
60
+ // Then, search for project-local config (overrides global)
61
+ const projectResult = await this.searchProjectConfig(searchFrom);
62
+ if (projectResult?.config && typeof projectResult.config === "object") {
63
+ // Accept partial configs without strict validation - merge will handle defaults
64
+ baseConfig = this.mergeConfigs(baseConfig, projectResult.config);
65
+ this.loadedFilepath = projectResult.filepath;
66
+ logger.debug({ filepath: projectResult.filepath }, "Loaded project config");
67
+ }
68
+ // Finally, apply environment variable overrides (highest priority)
69
+ this.config = this.applyEnvironmentOverrides(baseConfig);
70
+ return this.config;
71
+ }
72
+ /**
73
+ * Load the global config file from ~/.servherd/config.json
74
+ */
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ async loadGlobalConfig() {
77
+ try {
78
+ const result = await this.explorer.load(this.globalConfigPath);
79
+ if (result?.config) {
80
+ const parsed = GlobalConfigSchema.deepPartial().safeParse(result.config);
81
+ if (parsed.success) {
82
+ logger.debug({ filepath: this.globalConfigPath }, "Loaded global config");
83
+ return parsed.data;
84
+ }
85
+ logger.warn({ error: parsed.error }, "Invalid global config file, ignoring");
86
+ }
87
+ }
88
+ catch {
89
+ // Global config doesn't exist or is unreadable, that's fine
90
+ logger.debug("No global config found");
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * Search for project-local config starting from the given directory
96
+ */
97
+ async searchProjectConfig(searchFrom) {
98
+ try {
99
+ const result = await this.explorer.search(searchFrom);
100
+ // Don't return if it's the global config (we handle that separately)
101
+ if (result?.filepath === this.globalConfigPath) {
102
+ return null;
103
+ }
104
+ return result;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * Merge partial config into base config (supports deeply partial configs)
112
+ */
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ mergeConfigs(base, partial) {
115
+ return {
116
+ ...base,
117
+ ...partial,
118
+ portRange: {
119
+ ...base.portRange,
120
+ ...(partial.portRange || {}),
121
+ },
122
+ pm2: {
123
+ ...base.pm2,
124
+ ...(partial.pm2 || {}),
125
+ },
126
+ };
127
+ }
128
+ /**
129
+ * Save configuration to the global config file
130
+ */
131
+ async save(config) {
132
+ await ensureDir(this.globalConfigDir);
133
+ await writeJson(this.globalConfigPath, config, { spaces: 2 });
134
+ this.config = config;
135
+ }
136
+ /**
137
+ * Get a configuration value
138
+ */
139
+ get(key) {
140
+ return this.config[key];
141
+ }
142
+ /**
143
+ * Set a configuration value and persist to the global config file
144
+ */
145
+ async set(key, value) {
146
+ this.config[key] = value;
147
+ await this.save(this.config);
148
+ }
149
+ /**
150
+ * Get default configuration
151
+ */
152
+ getDefaults() {
153
+ return { ...DEFAULT_CONFIG };
154
+ }
155
+ /**
156
+ * Get the global config file path
157
+ */
158
+ getConfigPath() {
159
+ return this.globalConfigPath;
160
+ }
161
+ /**
162
+ * Get the path of the currently loaded config file (if any)
163
+ */
164
+ getLoadedConfigPath() {
165
+ return this.loadedFilepath;
166
+ }
167
+ /**
168
+ * Get all supported config file names for documentation
169
+ */
170
+ getSupportedConfigFiles() {
171
+ return [
172
+ "package.json (\"servherd\" key)",
173
+ ".servherdrc",
174
+ ".servherdrc.json",
175
+ ".servherdrc.yaml",
176
+ ".servherdrc.yml",
177
+ ".servherdrc.js",
178
+ ".servherdrc.cjs",
179
+ "servherd.config.js",
180
+ "servherd.config.cjs",
181
+ "~/.servherd/config.json (global)",
182
+ ];
183
+ }
184
+ /**
185
+ * Apply environment variable overrides to configuration
186
+ */
187
+ applyEnvironmentOverrides(config) {
188
+ const result = { ...config };
189
+ if (process.env.SERVHERD_HOSTNAME) {
190
+ result.hostname = process.env.SERVHERD_HOSTNAME;
191
+ }
192
+ if (process.env.SERVHERD_PROTOCOL) {
193
+ const protocol = process.env.SERVHERD_PROTOCOL;
194
+ if (protocol === "http" || protocol === "https") {
195
+ result.protocol = protocol;
196
+ }
197
+ }
198
+ if (process.env.SERVHERD_PORT_MIN) {
199
+ const min = parseInt(process.env.SERVHERD_PORT_MIN, 10);
200
+ if (!isNaN(min)) {
201
+ result.portRange = { ...result.portRange, min };
202
+ }
203
+ }
204
+ if (process.env.SERVHERD_PORT_MAX) {
205
+ const max = parseInt(process.env.SERVHERD_PORT_MAX, 10);
206
+ if (!isNaN(max)) {
207
+ result.portRange = { ...result.portRange, max };
208
+ }
209
+ }
210
+ if (process.env.SERVHERD_TEMP_DIR) {
211
+ result.tempDir = process.env.SERVHERD_TEMP_DIR;
212
+ }
213
+ if (process.env.SERVHERD_HTTPS_CERT) {
214
+ result.httpsCert = process.env.SERVHERD_HTTPS_CERT;
215
+ }
216
+ if (process.env.SERVHERD_HTTPS_KEY) {
217
+ result.httpsKey = process.env.SERVHERD_HTTPS_KEY;
218
+ }
219
+ return result;
220
+ }
221
+ /**
222
+ * Clear the cosmiconfig cache (useful for testing or after config changes)
223
+ */
224
+ clearCache() {
225
+ this.explorer.clearCaches();
226
+ }
227
+ }
@@ -0,0 +1,82 @@
1
+ import type { GlobalConfig } from "../types/config.js";
2
+ /**
3
+ * Result of port assignment, including whether a different port was assigned
4
+ */
5
+ export interface PortAssignmentResult {
6
+ port: number;
7
+ reassigned: boolean;
8
+ }
9
+ /**
10
+ * Service for deterministic port generation and port availability checking
11
+ * using FNV-1a hashing and detect-port library
12
+ */
13
+ export declare class PortService {
14
+ private portRange;
15
+ private usedPorts;
16
+ constructor(config: GlobalConfig);
17
+ /**
18
+ * Track a port as used (for CI mode sequential allocation)
19
+ * @param port - Port number to mark as used
20
+ */
21
+ trackUsedPort(port: number): void;
22
+ /**
23
+ * Clear all tracked used ports
24
+ */
25
+ clearUsedPorts(): void;
26
+ /**
27
+ * Generate a deterministic port number based on cwd and command
28
+ * Uses FNV-1a hash to ensure consistent port assignment for the same inputs
29
+ * @param cwd - Working directory of the server
30
+ * @param command - Command used to start the server
31
+ * @returns A port number within the configured range
32
+ */
33
+ generatePort(cwd: string, command: string): number;
34
+ /**
35
+ * Compute FNV-1a hash of the combined input string
36
+ * FNV-1a is a fast, non-cryptographic hash with good distribution
37
+ * @param cwd - Working directory
38
+ * @param command - Command string
39
+ * @returns A positive 32-bit integer hash
40
+ */
41
+ computeHash(cwd: string, command: string): number;
42
+ /**
43
+ * Check if a port is available for use
44
+ * @param port - Port number to check
45
+ * @returns true if port is available, false otherwise
46
+ */
47
+ isPortAvailable(port: number): Promise<boolean>;
48
+ /**
49
+ * Get an available port, starting from the preferred port
50
+ * If preferred is not available, searches for next available in range
51
+ * @param preferred - Preferred port number to try first
52
+ * @returns Object with assigned port and whether it was reassigned
53
+ */
54
+ getAvailablePort(preferred: number): Promise<PortAssignmentResult>;
55
+ /**
56
+ * Assign a port for a server, checking availability
57
+ * @param cwd - Working directory of the server
58
+ * @param command - Command used to start the server
59
+ * @param explicitPort - Optional explicit port to use (takes precedence)
60
+ * @param ciMode - If true, use sequential port allocation instead of deterministic
61
+ * @returns Object with assigned port and whether it was reassigned
62
+ */
63
+ assignPort(cwd: string, command: string, explicitPort?: number, ciMode?: boolean): Promise<PortAssignmentResult>;
64
+ /**
65
+ * Get next available port sequentially from the configured range
66
+ * Used in CI mode to avoid hash collisions and ensure predictable behavior
67
+ * @returns Object with assigned port and whether it was reassigned
68
+ */
69
+ private getNextAvailableSequential;
70
+ /**
71
+ * Validate that a port is within the configured range
72
+ * @param port - Port number to validate
73
+ * @throws ServherdError if port is outside range
74
+ */
75
+ validatePortInRange(port: number): void;
76
+ /**
77
+ * FNV-1a hash implementation
78
+ * @param str - String to hash
79
+ * @returns A positive 32-bit integer
80
+ */
81
+ private fnv1aHash;
82
+ }
@@ -0,0 +1,151 @@
1
+ import detectPort from "detect-port";
2
+ import { ServherdError, ServherdErrorCode } from "../types/errors.js";
3
+ /**
4
+ * Service for deterministic port generation and port availability checking
5
+ * using FNV-1a hashing and detect-port library
6
+ */
7
+ export class PortService {
8
+ portRange;
9
+ usedPorts = new Set();
10
+ constructor(config) {
11
+ this.portRange = config.portRange;
12
+ }
13
+ /**
14
+ * Track a port as used (for CI mode sequential allocation)
15
+ * @param port - Port number to mark as used
16
+ */
17
+ trackUsedPort(port) {
18
+ this.usedPorts.add(port);
19
+ }
20
+ /**
21
+ * Clear all tracked used ports
22
+ */
23
+ clearUsedPorts() {
24
+ this.usedPorts.clear();
25
+ }
26
+ /**
27
+ * Generate a deterministic port number based on cwd and command
28
+ * Uses FNV-1a hash to ensure consistent port assignment for the same inputs
29
+ * @param cwd - Working directory of the server
30
+ * @param command - Command used to start the server
31
+ * @returns A port number within the configured range
32
+ */
33
+ generatePort(cwd, command) {
34
+ const hash = this.computeHash(cwd, command);
35
+ const range = this.portRange.max - this.portRange.min + 1;
36
+ return this.portRange.min + (hash % range);
37
+ }
38
+ /**
39
+ * Compute FNV-1a hash of the combined input string
40
+ * FNV-1a is a fast, non-cryptographic hash with good distribution
41
+ * @param cwd - Working directory
42
+ * @param command - Command string
43
+ * @returns A positive 32-bit integer hash
44
+ */
45
+ computeHash(cwd, command) {
46
+ const input = `${cwd}:${command}`;
47
+ return this.fnv1aHash(input);
48
+ }
49
+ /**
50
+ * Check if a port is available for use
51
+ * @param port - Port number to check
52
+ * @returns true if port is available, false otherwise
53
+ */
54
+ async isPortAvailable(port) {
55
+ const availablePort = await detectPort(port);
56
+ return availablePort === port;
57
+ }
58
+ /**
59
+ * Get an available port, starting from the preferred port
60
+ * If preferred is not available, searches for next available in range
61
+ * @param preferred - Preferred port number to try first
62
+ * @returns Object with assigned port and whether it was reassigned
63
+ */
64
+ async getAvailablePort(preferred) {
65
+ // Try preferred port first
66
+ if (await this.isPortAvailable(preferred)) {
67
+ return { port: preferred, reassigned: false };
68
+ }
69
+ // Find next available port in range (from preferred+1 to max)
70
+ for (let port = preferred + 1; port <= this.portRange.max; port++) {
71
+ if (await this.isPortAvailable(port)) {
72
+ return { port, reassigned: true };
73
+ }
74
+ }
75
+ // Wrap around and check from min to preferred-1
76
+ for (let port = this.portRange.min; port < preferred; port++) {
77
+ if (await this.isPortAvailable(port)) {
78
+ return { port, reassigned: true };
79
+ }
80
+ }
81
+ throw new ServherdError(ServherdErrorCode.PORT_ALLOCATION_FAILED, `No available ports in range ${this.portRange.min}-${this.portRange.max}`);
82
+ }
83
+ /**
84
+ * Assign a port for a server, checking availability
85
+ * @param cwd - Working directory of the server
86
+ * @param command - Command used to start the server
87
+ * @param explicitPort - Optional explicit port to use (takes precedence)
88
+ * @param ciMode - If true, use sequential port allocation instead of deterministic
89
+ * @returns Object with assigned port and whether it was reassigned
90
+ */
91
+ async assignPort(cwd, command, explicitPort, ciMode = false) {
92
+ if (explicitPort !== undefined) {
93
+ this.validatePortInRange(explicitPort);
94
+ return this.getAvailablePort(explicitPort);
95
+ }
96
+ if (ciMode) {
97
+ return this.getNextAvailableSequential();
98
+ }
99
+ const preferred = this.generatePort(cwd, command);
100
+ return this.getAvailablePort(preferred);
101
+ }
102
+ /**
103
+ * Get next available port sequentially from the configured range
104
+ * Used in CI mode to avoid hash collisions and ensure predictable behavior
105
+ * @returns Object with assigned port and whether it was reassigned
106
+ */
107
+ async getNextAvailableSequential() {
108
+ let skippedPorts = false;
109
+ for (let port = this.portRange.min; port <= this.portRange.max; port++) {
110
+ // Skip ports we've already allocated in this session
111
+ if (this.usedPorts.has(port)) {
112
+ skippedPorts = true;
113
+ continue;
114
+ }
115
+ if (await this.isPortAvailable(port)) {
116
+ // If we skipped any ports (either tracked or unavailable), mark as reassigned
117
+ return { port, reassigned: skippedPorts };
118
+ }
119
+ skippedPorts = true;
120
+ }
121
+ throw new ServherdError(ServherdErrorCode.PORT_ALLOCATION_FAILED, `No available ports in range ${this.portRange.min}-${this.portRange.max}`);
122
+ }
123
+ /**
124
+ * Validate that a port is within the configured range
125
+ * @param port - Port number to validate
126
+ * @throws ServherdError if port is outside range
127
+ */
128
+ validatePortInRange(port) {
129
+ if (port < this.portRange.min || port > this.portRange.max) {
130
+ throw new ServherdError(ServherdErrorCode.PORT_OUT_OF_RANGE, `Port ${port} is outside configured range ${this.portRange.min}-${this.portRange.max}`);
131
+ }
132
+ }
133
+ /**
134
+ * FNV-1a hash implementation
135
+ * @param str - String to hash
136
+ * @returns A positive 32-bit integer
137
+ */
138
+ fnv1aHash(str) {
139
+ // FNV-1a parameters for 32-bit hash
140
+ const FNV_PRIME = 0x01000193;
141
+ const FNV_OFFSET_BASIS = 0x811c9dc5;
142
+ let hash = FNV_OFFSET_BASIS;
143
+ for (let i = 0; i < str.length; i++) {
144
+ hash ^= str.charCodeAt(i);
145
+ // Multiply by FNV prime and keep 32-bit
146
+ hash = Math.imul(hash, FNV_PRIME) >>> 0;
147
+ }
148
+ // Ensure positive integer
149
+ return hash >>> 0;
150
+ }
151
+ }
@@ -0,0 +1,61 @@
1
+ import type { PM2ProcessDescription, PM2StartOptions, PM2Process } from "../types/pm2.js";
2
+ import type { ServerStatus } from "../types/registry.js";
3
+ /**
4
+ * Service for managing processes via PM2
5
+ */
6
+ export declare class ProcessService {
7
+ private connected;
8
+ /**
9
+ * Connect to PM2 daemon
10
+ */
11
+ connect(): Promise<void>;
12
+ /**
13
+ * Disconnect from PM2 daemon
14
+ */
15
+ disconnect(): void;
16
+ /**
17
+ * Ensure connected to PM2
18
+ */
19
+ private ensureConnected;
20
+ /**
21
+ * Start a new process
22
+ */
23
+ start(options: PM2StartOptions): Promise<PM2Process[]>;
24
+ /**
25
+ * Stop a process by name
26
+ */
27
+ stop(name: string): Promise<PM2Process[]>;
28
+ /**
29
+ * Restart a process by name
30
+ */
31
+ restart(name: string): Promise<PM2Process[]>;
32
+ /**
33
+ * Delete a process by name
34
+ */
35
+ delete(name: string): Promise<PM2Process[]>;
36
+ /**
37
+ * Get process description
38
+ */
39
+ describe(name: string): Promise<PM2ProcessDescription | undefined>;
40
+ /**
41
+ * List all PM2 processes
42
+ */
43
+ list(): Promise<PM2ProcessDescription[]>;
44
+ /**
45
+ * List only servherd-managed processes
46
+ */
47
+ listServherdProcesses(): Promise<PM2ProcessDescription[]>;
48
+ /**
49
+ * Get the status of a process
50
+ */
51
+ getStatus(name: string): Promise<ServerStatus>;
52
+ /**
53
+ * Check if connected to PM2
54
+ */
55
+ isConnected(): boolean;
56
+ /**
57
+ * Flush (clear) logs for a process or all processes.
58
+ * @param name - Process name to flush logs for, or undefined/all for all processes
59
+ */
60
+ flush(name?: string): Promise<void>;
61
+ }