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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for detecting configuration drift in servers.
|
|
3
|
+
* Drift occurs when config values have changed since a server was started.
|
|
4
|
+
*/
|
|
5
|
+
import { extractVariables, TEMPLATE_VAR_TO_CONFIG_KEY } from "./template.js";
|
|
6
|
+
/**
|
|
7
|
+
* Extract the config keys used by a command template
|
|
8
|
+
* @param command - The command template string
|
|
9
|
+
* @returns Array of config keys used (e.g., ["hostname", "httpsCert"])
|
|
10
|
+
*/
|
|
11
|
+
export function extractUsedConfigKeys(command) {
|
|
12
|
+
const templateVars = extractVariables(command);
|
|
13
|
+
const configKeys = [];
|
|
14
|
+
for (const varName of templateVars) {
|
|
15
|
+
const configKey = TEMPLATE_VAR_TO_CONFIG_KEY[varName];
|
|
16
|
+
if (configKey) {
|
|
17
|
+
configKeys.push(configKey);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return [...new Set(configKeys)]; // Deduplicate
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a config snapshot containing only the values used by a server
|
|
24
|
+
* @param config - Current global config
|
|
25
|
+
* @param usedConfigKeys - Config keys used by the server
|
|
26
|
+
* @returns Config snapshot with only relevant values
|
|
27
|
+
*/
|
|
28
|
+
export function createConfigSnapshot(config, usedConfigKeys) {
|
|
29
|
+
const snapshot = {};
|
|
30
|
+
for (const key of usedConfigKeys) {
|
|
31
|
+
if (key === "hostname") {
|
|
32
|
+
snapshot.hostname = config.hostname;
|
|
33
|
+
}
|
|
34
|
+
else if (key === "httpsCert") {
|
|
35
|
+
snapshot.httpsCert = config.httpsCert;
|
|
36
|
+
}
|
|
37
|
+
else if (key === "httpsKey") {
|
|
38
|
+
snapshot.httpsKey = config.httpsKey;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Detect config drift for a server
|
|
45
|
+
* @param server - Server entry from registry
|
|
46
|
+
* @param currentConfig - Current global config
|
|
47
|
+
* @returns Drift detection result
|
|
48
|
+
*/
|
|
49
|
+
export function detectDrift(server, currentConfig) {
|
|
50
|
+
const driftedValues = [];
|
|
51
|
+
// If no snapshot exists, we can't detect drift
|
|
52
|
+
if (!server.configSnapshot || !server.usedConfigKeys) {
|
|
53
|
+
return { hasDrift: false, driftedValues: [] };
|
|
54
|
+
}
|
|
55
|
+
for (const configKey of server.usedConfigKeys) {
|
|
56
|
+
let startedWith;
|
|
57
|
+
let currentValue;
|
|
58
|
+
let templateVar;
|
|
59
|
+
if (configKey === "hostname") {
|
|
60
|
+
startedWith = server.configSnapshot.hostname;
|
|
61
|
+
currentValue = currentConfig.hostname;
|
|
62
|
+
templateVar = "hostname";
|
|
63
|
+
}
|
|
64
|
+
else if (configKey === "httpsCert") {
|
|
65
|
+
startedWith = server.configSnapshot.httpsCert;
|
|
66
|
+
currentValue = currentConfig.httpsCert;
|
|
67
|
+
templateVar = "https-cert";
|
|
68
|
+
}
|
|
69
|
+
else if (configKey === "httpsKey") {
|
|
70
|
+
startedWith = server.configSnapshot.httpsKey;
|
|
71
|
+
currentValue = currentConfig.httpsKey;
|
|
72
|
+
templateVar = "https-key";
|
|
73
|
+
}
|
|
74
|
+
if (templateVar && startedWith !== currentValue) {
|
|
75
|
+
driftedValues.push({
|
|
76
|
+
configKey,
|
|
77
|
+
templateVar,
|
|
78
|
+
startedWith,
|
|
79
|
+
currentValue,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
hasDrift: driftedValues.length > 0,
|
|
85
|
+
driftedValues,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Find all servers that use a specific config key
|
|
90
|
+
* @param servers - Array of server entries
|
|
91
|
+
* @param configKey - The config key to search for
|
|
92
|
+
* @returns Array of servers using that config key
|
|
93
|
+
*/
|
|
94
|
+
export function findServersUsingConfigKey(servers, configKey) {
|
|
95
|
+
return servers.filter(server => server.usedConfigKeys?.includes(configKey));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find all servers with config drift
|
|
99
|
+
* @param servers - Array of server entries
|
|
100
|
+
* @param currentConfig - Current global config
|
|
101
|
+
* @returns Array of servers with drift and their drift details
|
|
102
|
+
*/
|
|
103
|
+
export function findServersWithDrift(servers, currentConfig) {
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const server of servers) {
|
|
106
|
+
const drift = detectDrift(server, currentConfig);
|
|
107
|
+
if (drift.hasDrift) {
|
|
108
|
+
results.push({ server, drift });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Format drift information for display
|
|
115
|
+
* @param drift - Drift result to format
|
|
116
|
+
* @returns Formatted string describing the drift
|
|
117
|
+
*/
|
|
118
|
+
export function formatDrift(drift) {
|
|
119
|
+
if (!drift.hasDrift) {
|
|
120
|
+
return "No config drift";
|
|
121
|
+
}
|
|
122
|
+
const lines = drift.driftedValues.map(d => {
|
|
123
|
+
const started = d.startedWith ?? "(not set)";
|
|
124
|
+
const current = d.currentValue ?? "(not set)";
|
|
125
|
+
return ` ${d.configKey}: "${started}" → "${current}"`;
|
|
126
|
+
});
|
|
127
|
+
return `Config drift detected:\n${lines.join("\n")}`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ServherdError, ServherdErrorCode } from "../types/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a ServherdError from an unknown error.
|
|
4
|
+
* Useful for wrapping errors from external libraries.
|
|
5
|
+
*/
|
|
6
|
+
export declare function wrapError(error: unknown, code: ServherdErrorCode, message?: string): ServherdError;
|
|
7
|
+
/**
|
|
8
|
+
* Assert that a condition is true, throwing a ServherdError if not.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertServherd(condition: unknown, code: ServherdErrorCode, message: string): asserts condition;
|
|
11
|
+
/**
|
|
12
|
+
* Try to execute a function, returning a result object.
|
|
13
|
+
* Useful for operations where you want to handle errors gracefully.
|
|
14
|
+
*/
|
|
15
|
+
export declare function tryAsync<T>(fn: () => Promise<T>): Promise<{
|
|
16
|
+
success: true;
|
|
17
|
+
value: T;
|
|
18
|
+
} | {
|
|
19
|
+
success: false;
|
|
20
|
+
error: ServherdError;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ServherdError, ServherdErrorCode, isServherdError } from "../types/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a ServherdError from an unknown error.
|
|
4
|
+
* Useful for wrapping errors from external libraries.
|
|
5
|
+
*/
|
|
6
|
+
export function wrapError(error, code, message) {
|
|
7
|
+
if (isServherdError(error)) {
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
11
|
+
const finalMessage = message || originalMessage;
|
|
12
|
+
return new ServherdError(code, finalMessage, {
|
|
13
|
+
cause: error instanceof Error ? error : undefined,
|
|
14
|
+
stderr: error instanceof Error ? error.message : undefined,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Assert that a condition is true, throwing a ServherdError if not.
|
|
19
|
+
*/
|
|
20
|
+
export function assertServherd(condition, code, message) {
|
|
21
|
+
if (!condition) {
|
|
22
|
+
throw new ServherdError(code, message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Try to execute a function, returning a result object.
|
|
27
|
+
* Useful for operations where you want to handle errors gracefully.
|
|
28
|
+
*/
|
|
29
|
+
export async function tryAsync(fn) {
|
|
30
|
+
try {
|
|
31
|
+
const value = await fn();
|
|
32
|
+
return { success: true, value };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const wrappedError = wrapError(error, ServherdErrorCode.UNKNOWN_ERROR);
|
|
36
|
+
return { success: false, error: wrappedError };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follow a log file and emit new lines as they are written.
|
|
3
|
+
* Similar to `tail -f` behavior.
|
|
4
|
+
*
|
|
5
|
+
* @param logPath - Path to the log file to follow
|
|
6
|
+
* @param signal - AbortSignal to stop following
|
|
7
|
+
* @param onLine - Callback for each new line
|
|
8
|
+
* @returns Promise that resolves when following is stopped
|
|
9
|
+
*/
|
|
10
|
+
export declare function followLog(logPath: string, signal: AbortSignal, onLine: (line: string) => void): Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { watch, createReadStream } from "fs";
|
|
2
|
+
import { stat } from "fs/promises";
|
|
3
|
+
import { createInterface } from "readline";
|
|
4
|
+
/**
|
|
5
|
+
* Follow a log file and emit new lines as they are written.
|
|
6
|
+
* Similar to `tail -f` behavior.
|
|
7
|
+
*
|
|
8
|
+
* @param logPath - Path to the log file to follow
|
|
9
|
+
* @param signal - AbortSignal to stop following
|
|
10
|
+
* @param onLine - Callback for each new line
|
|
11
|
+
* @returns Promise that resolves when following is stopped
|
|
12
|
+
*/
|
|
13
|
+
export async function followLog(logPath, signal, onLine) {
|
|
14
|
+
// If already aborted, return immediately
|
|
15
|
+
if (signal.aborted) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
// Get initial file size to start reading from the end
|
|
19
|
+
let position;
|
|
20
|
+
try {
|
|
21
|
+
const stats = await stat(logPath);
|
|
22
|
+
position = stats.size;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// If file doesn't exist yet, start from 0
|
|
26
|
+
position = 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read new lines from the file starting at the current position.
|
|
30
|
+
*/
|
|
31
|
+
const readNewLines = async () => {
|
|
32
|
+
if (signal.aborted) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const currentStats = await stat(logPath);
|
|
37
|
+
const currentSize = currentStats.size;
|
|
38
|
+
// File was truncated or no new content
|
|
39
|
+
if (currentSize <= position) {
|
|
40
|
+
// If file was truncated, reset position
|
|
41
|
+
if (currentSize < position) {
|
|
42
|
+
position = 0;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Read new content
|
|
47
|
+
const stream = createReadStream(logPath, {
|
|
48
|
+
start: position,
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
});
|
|
51
|
+
const rl = createInterface({
|
|
52
|
+
input: stream,
|
|
53
|
+
crlfDelay: Infinity,
|
|
54
|
+
});
|
|
55
|
+
for await (const line of rl) {
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
stream.destroy();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
// Only emit non-empty lines
|
|
61
|
+
if (line.trim()) {
|
|
62
|
+
onLine(line);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Update position to end of file
|
|
66
|
+
position = currentSize;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore errors reading the file (it may have been deleted/rotated)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
// Set up file watcher
|
|
74
|
+
const watcher = watch(logPath, async (eventType) => {
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (eventType === "change") {
|
|
79
|
+
await readNewLines();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Handle watcher errors (e.g., file doesn't exist)
|
|
83
|
+
watcher.on("error", () => {
|
|
84
|
+
// Ignore errors - the file may be rotated or deleted
|
|
85
|
+
});
|
|
86
|
+
// Clean up on abort
|
|
87
|
+
const abortHandler = () => {
|
|
88
|
+
watcher.close();
|
|
89
|
+
resolve();
|
|
90
|
+
};
|
|
91
|
+
if (signal.aborted) {
|
|
92
|
+
abortHandler();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
signal.addEventListener("abort", abortHandler);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pino, { type Logger } from "pino";
|
|
2
|
+
export type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace" | "silent";
|
|
3
|
+
export interface LoggerOptions {
|
|
4
|
+
level?: LogLevel;
|
|
5
|
+
pretty?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Create a configured pino logger instance
|
|
9
|
+
*/
|
|
10
|
+
export declare function createLogger(options: LoggerOptions): Logger;
|
|
11
|
+
export declare const logger: pino.Logger;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
/**
|
|
3
|
+
* Create a configured pino logger instance
|
|
4
|
+
*/
|
|
5
|
+
export function createLogger(options) {
|
|
6
|
+
const level = options.level ?? "info";
|
|
7
|
+
const pretty = options.pretty ?? process.env.NODE_ENV !== "production";
|
|
8
|
+
if (pretty) {
|
|
9
|
+
return pino({
|
|
10
|
+
level,
|
|
11
|
+
transport: {
|
|
12
|
+
target: "pino-pretty",
|
|
13
|
+
options: {
|
|
14
|
+
colorize: true,
|
|
15
|
+
translateTime: "SYS:standard",
|
|
16
|
+
ignore: "pid,hostname",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return pino({ level });
|
|
22
|
+
}
|
|
23
|
+
// Export a default logger instance
|
|
24
|
+
export const logger = createLogger({ level: "info" });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a human-readable name in adjective-noun format
|
|
3
|
+
* @param existing - Set of existing names to avoid duplicates
|
|
4
|
+
* @param maxAttempts - Maximum number of attempts to generate a unique name
|
|
5
|
+
* @returns A unique human-readable name
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateName(existing?: Set<string>, maxAttempts?: number): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FlexiHumanHash } from "flexi-human-hash";
|
|
2
|
+
const fhh = new FlexiHumanHash("{{adjective}}-{{noun}}");
|
|
3
|
+
/**
|
|
4
|
+
* Generate a human-readable name in adjective-noun format
|
|
5
|
+
* @param existing - Set of existing names to avoid duplicates
|
|
6
|
+
* @param maxAttempts - Maximum number of attempts to generate a unique name
|
|
7
|
+
* @returns A unique human-readable name
|
|
8
|
+
*/
|
|
9
|
+
export function generateName(existing, maxAttempts = 100) {
|
|
10
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
11
|
+
const name = fhh.hash();
|
|
12
|
+
if (!existing || !existing.has(name)) {
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback: add a unique suffix if we can't find a unique name
|
|
17
|
+
const baseName = fhh.hash();
|
|
18
|
+
const timestamp = Date.now().toString(36).slice(-4);
|
|
19
|
+
return `${baseName}-${timestamp}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template variable substitution engine for command templates
|
|
3
|
+
* Supports {{port}}, {{hostname}}, {{url}}, {{https-cert}}, {{https-key}} and other variables
|
|
4
|
+
*/
|
|
5
|
+
import type { GlobalConfig } from "../types/config.js";
|
|
6
|
+
export interface TemplateVariables {
|
|
7
|
+
port?: number | string;
|
|
8
|
+
hostname?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
"https-cert"?: string;
|
|
11
|
+
"https-key"?: string;
|
|
12
|
+
[key: string]: string | number | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render a template string by substituting variables
|
|
16
|
+
* @param template - The template string containing {{variable}} placeholders
|
|
17
|
+
* @param variables - Object containing variable values to substitute
|
|
18
|
+
* @returns The rendered string with variables replaced
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderTemplate(template: string, variables: TemplateVariables): string;
|
|
21
|
+
/**
|
|
22
|
+
* Extract all unique variable names from a template string
|
|
23
|
+
* @param template - The template string to analyze
|
|
24
|
+
* @returns Array of unique variable names found in the template
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractVariables(template: string): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Parse an array of KEY=VALUE strings into a Record
|
|
29
|
+
* @param envStrings - Array of strings in KEY=VALUE format
|
|
30
|
+
* @returns Record with parsed key-value pairs
|
|
31
|
+
* @throws Error if a string is not in valid KEY=VALUE format
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseEnvStrings(envStrings: string[]): Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Render template variables in all values of an env object
|
|
36
|
+
* @param env - Record of environment variables with potential template values
|
|
37
|
+
* @param variables - Template variables to substitute
|
|
38
|
+
* @returns New record with all values having templates substituted
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderEnvTemplates(env: Record<string, string>, variables: TemplateVariables): Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Generate template variables from configuration
|
|
43
|
+
* @param config - Global configuration object
|
|
44
|
+
* @param port - The port number for this server
|
|
45
|
+
* @returns TemplateVariables object ready for template substitution
|
|
46
|
+
*/
|
|
47
|
+
export declare function getTemplateVariables(config: GlobalConfig, port: number): TemplateVariables;
|
|
48
|
+
/**
|
|
49
|
+
* Mapping from template variable names to their corresponding config keys
|
|
50
|
+
* Variables not in this map are auto-generated (port, url) and can't be configured
|
|
51
|
+
*/
|
|
52
|
+
export declare const TEMPLATE_VAR_TO_CONFIG_KEY: Record<string, keyof GlobalConfig | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Human-readable prompts for each configurable template variable
|
|
55
|
+
*/
|
|
56
|
+
export declare const TEMPLATE_VAR_PROMPTS: Record<string, string>;
|
|
57
|
+
/**
|
|
58
|
+
* Information about a missing template variable
|
|
59
|
+
*/
|
|
60
|
+
export interface MissingVariable {
|
|
61
|
+
/** The template variable name (e.g., "https-cert") */
|
|
62
|
+
templateVar: string;
|
|
63
|
+
/** The config key to set (e.g., "httpsCert"), or null if not configurable */
|
|
64
|
+
configKey: keyof GlobalConfig | null;
|
|
65
|
+
/** Human-readable prompt for the user */
|
|
66
|
+
prompt: string;
|
|
67
|
+
/** Whether this variable can be configured by the user */
|
|
68
|
+
configurable: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Find template variables that are used but have empty or missing values
|
|
72
|
+
* @param template - The template string to analyze
|
|
73
|
+
* @param variables - The current template variables
|
|
74
|
+
* @returns Array of MissingVariable objects for variables that need values
|
|
75
|
+
*/
|
|
76
|
+
export declare function findMissingVariables(template: string, variables: TemplateVariables): MissingVariable[];
|
|
77
|
+
/**
|
|
78
|
+
* Format missing variables as a user-friendly error message
|
|
79
|
+
* @param missing - Array of missing variables
|
|
80
|
+
* @returns Formatted error message string
|
|
81
|
+
*/
|
|
82
|
+
export declare function formatMissingVariablesError(missing: MissingVariable[]): string;
|
|
83
|
+
/**
|
|
84
|
+
* Format missing variables as an MCP-friendly error message
|
|
85
|
+
* @param missing - Array of missing variables
|
|
86
|
+
* @returns Formatted error message for MCP tool response
|
|
87
|
+
*/
|
|
88
|
+
export declare function formatMissingVariablesForMCP(missing: MissingVariable[]): string;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template variable substitution engine for command templates
|
|
3
|
+
* Supports {{port}}, {{hostname}}, {{url}}, {{https-cert}}, {{https-key}} and other variables
|
|
4
|
+
*/
|
|
5
|
+
// Regex to match template variables like {{port}}, {{ port }}, or {{https-cert}}
|
|
6
|
+
const TEMPLATE_REGEX = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
7
|
+
/**
|
|
8
|
+
* Render a template string by substituting variables
|
|
9
|
+
* @param template - The template string containing {{variable}} placeholders
|
|
10
|
+
* @param variables - Object containing variable values to substitute
|
|
11
|
+
* @returns The rendered string with variables replaced
|
|
12
|
+
*/
|
|
13
|
+
export function renderTemplate(template, variables) {
|
|
14
|
+
return template.replace(TEMPLATE_REGEX, (match, varName) => {
|
|
15
|
+
const value = variables[varName];
|
|
16
|
+
if (value !== undefined) {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
// Leave unresolved variables unchanged
|
|
20
|
+
return match;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract all unique variable names from a template string
|
|
25
|
+
* @param template - The template string to analyze
|
|
26
|
+
* @returns Array of unique variable names found in the template
|
|
27
|
+
*/
|
|
28
|
+
export function extractVariables(template) {
|
|
29
|
+
const variables = new Set();
|
|
30
|
+
let match;
|
|
31
|
+
// Reset regex state
|
|
32
|
+
TEMPLATE_REGEX.lastIndex = 0;
|
|
33
|
+
while ((match = TEMPLATE_REGEX.exec(template)) !== null) {
|
|
34
|
+
variables.add(match[1]);
|
|
35
|
+
}
|
|
36
|
+
return Array.from(variables);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse an array of KEY=VALUE strings into a Record
|
|
40
|
+
* @param envStrings - Array of strings in KEY=VALUE format
|
|
41
|
+
* @returns Record with parsed key-value pairs
|
|
42
|
+
* @throws Error if a string is not in valid KEY=VALUE format
|
|
43
|
+
*/
|
|
44
|
+
export function parseEnvStrings(envStrings) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const str of envStrings) {
|
|
47
|
+
const equalsIndex = str.indexOf("=");
|
|
48
|
+
if (equalsIndex === -1) {
|
|
49
|
+
throw new Error(`Invalid environment variable format: "${str}". Expected KEY=VALUE format.`);
|
|
50
|
+
}
|
|
51
|
+
const key = str.slice(0, equalsIndex);
|
|
52
|
+
const value = str.slice(equalsIndex + 1);
|
|
53
|
+
if (!key) {
|
|
54
|
+
throw new Error(`Invalid environment variable format: "${str}". Key cannot be empty.`);
|
|
55
|
+
}
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render template variables in all values of an env object
|
|
62
|
+
* @param env - Record of environment variables with potential template values
|
|
63
|
+
* @param variables - Template variables to substitute
|
|
64
|
+
* @returns New record with all values having templates substituted
|
|
65
|
+
*/
|
|
66
|
+
export function renderEnvTemplates(env, variables) {
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const [key, value] of Object.entries(env)) {
|
|
69
|
+
result[key] = renderTemplate(value, variables);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate template variables from configuration
|
|
75
|
+
* @param config - Global configuration object
|
|
76
|
+
* @param port - The port number for this server
|
|
77
|
+
* @returns TemplateVariables object ready for template substitution
|
|
78
|
+
*/
|
|
79
|
+
export function getTemplateVariables(config, port) {
|
|
80
|
+
return {
|
|
81
|
+
port,
|
|
82
|
+
hostname: config.hostname,
|
|
83
|
+
url: `${config.protocol}://${config.hostname}:${port}`,
|
|
84
|
+
"https-cert": config.httpsCert ?? "",
|
|
85
|
+
"https-key": config.httpsKey ?? "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Mapping from template variable names to their corresponding config keys
|
|
90
|
+
* Variables not in this map are auto-generated (port, url) and can't be configured
|
|
91
|
+
*/
|
|
92
|
+
export const TEMPLATE_VAR_TO_CONFIG_KEY = {
|
|
93
|
+
"port": null, // Auto-assigned, not configurable
|
|
94
|
+
"hostname": "hostname",
|
|
95
|
+
"url": null, // Auto-generated from protocol/hostname/port
|
|
96
|
+
"https-cert": "httpsCert",
|
|
97
|
+
"https-key": "httpsKey",
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Human-readable prompts for each configurable template variable
|
|
101
|
+
*/
|
|
102
|
+
export const TEMPLATE_VAR_PROMPTS = {
|
|
103
|
+
"hostname": "Server hostname (e.g., localhost, 0.0.0.0):",
|
|
104
|
+
"https-cert": "Path to HTTPS certificate file:",
|
|
105
|
+
"https-key": "Path to HTTPS private key file:",
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Find template variables that are used but have empty or missing values
|
|
109
|
+
* @param template - The template string to analyze
|
|
110
|
+
* @param variables - The current template variables
|
|
111
|
+
* @returns Array of MissingVariable objects for variables that need values
|
|
112
|
+
*/
|
|
113
|
+
export function findMissingVariables(template, variables) {
|
|
114
|
+
const usedVars = extractVariables(template);
|
|
115
|
+
const missing = [];
|
|
116
|
+
for (const varName of usedVars) {
|
|
117
|
+
const value = variables[varName];
|
|
118
|
+
// Check if the value is empty, undefined, or an empty string
|
|
119
|
+
if (value === undefined || value === "" || value === null) {
|
|
120
|
+
const configKey = TEMPLATE_VAR_TO_CONFIG_KEY[varName] ?? null;
|
|
121
|
+
const configurable = configKey !== null;
|
|
122
|
+
const prompt = TEMPLATE_VAR_PROMPTS[varName] ?? `Value for ${varName}:`;
|
|
123
|
+
missing.push({
|
|
124
|
+
templateVar: varName,
|
|
125
|
+
configKey,
|
|
126
|
+
prompt,
|
|
127
|
+
configurable,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return missing;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Format missing variables as a user-friendly error message
|
|
135
|
+
* @param missing - Array of missing variables
|
|
136
|
+
* @returns Formatted error message string
|
|
137
|
+
*/
|
|
138
|
+
export function formatMissingVariablesError(missing) {
|
|
139
|
+
if (missing.length === 0) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
const lines = [
|
|
143
|
+
"The following template variables are used but not configured:",
|
|
144
|
+
"",
|
|
145
|
+
];
|
|
146
|
+
for (const v of missing) {
|
|
147
|
+
if (v.configurable) {
|
|
148
|
+
lines.push(` {{${v.templateVar}}} - Set with: servherd config --set ${v.configKey} --value <value>`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
lines.push(` {{${v.templateVar}}} - This variable is auto-generated and cannot be configured directly`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Format missing variables as an MCP-friendly error message
|
|
158
|
+
* @param missing - Array of missing variables
|
|
159
|
+
* @returns Formatted error message for MCP tool response
|
|
160
|
+
*/
|
|
161
|
+
export function formatMissingVariablesForMCP(missing) {
|
|
162
|
+
if (missing.length === 0) {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
const configurable = missing.filter(v => v.configurable);
|
|
166
|
+
if (configurable.length === 0) {
|
|
167
|
+
return "Template uses auto-generated variables that have no value. This may indicate an internal error.";
|
|
168
|
+
}
|
|
169
|
+
const lines = [
|
|
170
|
+
"Cannot start server: required configuration is missing.",
|
|
171
|
+
"",
|
|
172
|
+
"The command uses template variables that are not configured:",
|
|
173
|
+
];
|
|
174
|
+
for (const v of configurable) {
|
|
175
|
+
lines.push(` - {{${v.templateVar}}}: Use servherd_config tool with set="${v.configKey}" and value="<path or value>"`);
|
|
176
|
+
}
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push("Please configure these values first, then retry the start command.");
|
|
179
|
+
return lines.join("\n");
|
|
180
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a time filter string into a Date object.
|
|
3
|
+
* Supports duration format (1h, 30m, 2d, 1w, 30s) or ISO date strings.
|
|
4
|
+
*
|
|
5
|
+
* @param input - Time filter string to parse
|
|
6
|
+
* @returns Date object representing the cutoff time
|
|
7
|
+
* @throws ServherdError if the format is invalid
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseTimeFilter(input: string): Date;
|
|
10
|
+
/**
|
|
11
|
+
* Filter log lines by timestamp, keeping only those after the given date.
|
|
12
|
+
* Lines without timestamps (or where the parser returns null) are included.
|
|
13
|
+
*
|
|
14
|
+
* @param lines - Array of log lines to filter
|
|
15
|
+
* @param since - Only include logs from this date onward
|
|
16
|
+
* @param parseTimestamp - Function to extract a Date from a log line (returns null if no timestamp)
|
|
17
|
+
* @returns Filtered array of log lines
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterLogsByTime(lines: string[], since: Date, parseTimestamp: (line: string) => Date | null): string[];
|