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,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[];