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,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
|
+
}
|