• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / servherd / 20961701317

13 Jan 2026 03:07PM UTC coverage: 82.727% (+1.2%) from 81.563%
20961701317

push

github

apowers313
test: improved test coverage to 81.56%

901 of 1027 branches covered (87.73%)

Branch coverage included in aggregate %.

3601 of 4415 relevant lines covered (81.56%)

13.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.5
/src/cli/commands/restart.ts
1
import { RegistryService } from "../../services/registry.service.js";
1✔
2
import { ProcessService } from "../../services/process.service.js";
3
import { ConfigService } from "../../services/config.service.js";
4
import type { ServerEntry, ServerStatus } from "../../types/registry.js";
5
import type { GlobalConfig } from "../../types/config.js";
6
import { formatRestartResult, formatError } from "../output/formatters.js";
7
import { formatAsJson, formatErrorAsJson } from "../output/json-formatter.js";
8
import { logger } from "../../utils/logger.js";
9
import { renderTemplate, renderEnvTemplates, getTemplateVariables, type TemplateContext } from "../../utils/template.js";
10
import {
11
  extractUsedConfigKeys,
12
  createConfigSnapshot,
13
  detectDrift,
14
} from "../../utils/config-drift.js";
15
import { ServherdError, ServherdErrorCode } from "../../types/errors.js";
16

17
export interface RestartCommandOptions {
18
  name?: string;
19
  all?: boolean;
20
  tag?: string;
21
}
22

23
export interface RestartResult {
24
  name: string;
25
  success: boolean;
26
  status?: ServerStatus;
27
  message?: string;
28
  configRefreshed?: boolean;
29
}
30

31
/**
32
 * Build template context for server lookups
33
 */
34
function buildTemplateContext(
14✔
35
  registryService: RegistryService,
14✔
36
  cwd: string,
14✔
37
): TemplateContext {
14✔
38
  return {
14✔
39
    cwd,
14✔
40
    lookupServer: (name: string, lookupCwd?: string) => {
14✔
41
      return registryService.findByCwdAndName(lookupCwd ?? cwd, name);
×
42
    },
×
43
  };
14✔
44
}
14✔
45

46
/**
47
 * Re-resolve a server's command template with current config values
48
 * Updates the registry with new resolved command and config snapshot
49
 */
50
async function refreshServerConfig(
1✔
51
  server: ServerEntry,
1✔
52
  config: GlobalConfig,
1✔
53
  registryService: RegistryService,
1✔
54
  templateContext: TemplateContext,
1✔
55
): Promise<{ resolvedCommand: string; configSnapshot: ReturnType<typeof createConfigSnapshot> }> {
1✔
56
  // Get template variables with current config
57
  const templateVars = getTemplateVariables(config, server.port);
1✔
58

59
  // Re-resolve the command template
60
  const resolvedCommand = renderTemplate(server.command, templateVars, templateContext);
1✔
61

62
  // Re-resolve environment variables if any
63
  const resolvedEnv = server.env
1✔
64
    ? renderEnvTemplates(server.env, templateVars, templateContext)
1!
65
    : {};
×
66

67
  // Extract new used config keys and create new snapshot
68
  const usedConfigKeys = extractUsedConfigKeys(server.command);
1✔
69
  const configSnapshot = createConfigSnapshot(config, usedConfigKeys, server.command);
1✔
70

71
  // Update the registry
72
  await registryService.updateServer(server.id, {
1✔
73
    resolvedCommand,
1✔
74
    env: resolvedEnv,
1✔
75
    usedConfigKeys,
1✔
76
    configSnapshot,
1✔
77
  });
1✔
78

79
  return { resolvedCommand, configSnapshot };
1✔
80
}
1✔
81

82
/**
83
 * Execute the restart command for a single server
84
 */
85
export async function executeRestart(options: RestartCommandOptions): Promise<RestartResult | RestartResult[]> {
20✔
86
  const registryService = new RegistryService();
20✔
87
  const processService = new ProcessService();
20✔
88
  const configService = new ConfigService();
20✔
89

90
  try {
20✔
91
    // Load registry and config
92
    await registryService.load();
20✔
93
    const config = await configService.load();
20✔
94

95
    // Connect to PM2
96
    await processService.connect();
20✔
97

98
    // Determine which servers to restart
99
    let servers: ServerEntry[] = [];
20✔
100

101
    if (options.all) {
20✔
102
      servers = registryService.listServers();
3✔
103
    } else if (options.tag) {
20✔
104
      servers = registryService.listServers({ tag: options.tag });
1✔
105
    } else if (options.name) {
17✔
106
      const server = registryService.findByName(options.name);
15✔
107
      if (!server) {
15✔
108
        throw new ServherdError(
6✔
109
          ServherdErrorCode.SERVER_NOT_FOUND,
6✔
110
          `Server "${options.name}" not found`,
6✔
111
        );
6✔
112
      }
6✔
113
      servers = [server];
9✔
114
    } else {
16✔
115
      throw new ServherdError(
1✔
116
        ServherdErrorCode.COMMAND_MISSING_ARGUMENT,
1✔
117
        "Either --name, --all, or --tag must be specified",
1✔
118
      );
1✔
119
    }
1✔
120

121
    // Restart all matched servers
122
    const results: RestartResult[] = [];
13✔
123

124
    for (const server of servers) {
14✔
125
      try {
14✔
126
        let configRefreshed = false;
14✔
127

128
        // Build template context for this server's cwd
129
        const templateContext = buildTemplateContext(registryService, server.cwd);
14✔
130

131
        // Check if we should refresh config on restart (on-start mode)
132
        if (config.refreshOnChange === "on-start") {
14✔
133
          const drift = detectDrift(server, config);
7✔
134
          if (drift.hasDrift) {
7✔
135
            // Re-resolve command with new config values
136
            const { resolvedCommand } = await refreshServerConfig(server, config, registryService, templateContext);
1✔
137
            configRefreshed = true;
1✔
138

139
            // Delete old process and start with new command
140
            try {
1✔
141
              await processService.delete(server.pm2Name);
1✔
142
            } catch {
1!
143
              // Process might not exist
144
            }
×
145

146
            // Parse the resolved command to extract script and args
147
            const parts = resolvedCommand.trim().split(/\s+/);
1✔
148
            const script = parts[0] || "node";
1!
149
            const args = parts.slice(1);
1✔
150

151
            // Get the updated server entry for current env
152
            const updatedServer = registryService.findById(server.id);
1✔
153
            const env = updatedServer?.env ?? server.env;
1!
154

155
            await processService.start({
1✔
156
              name: server.pm2Name,
1✔
157
              script,
1✔
158
              args,
1✔
159
              cwd: server.cwd,
1✔
160
              env: {
1✔
161
                ...env,
1✔
162
                PORT: String(server.port),
1✔
163
              },
1✔
164
            });
1✔
165
          } else {
7✔
166
            // No drift, just restart normally
167
            await processService.restart(server.pm2Name);
6✔
168
          }
6✔
169
        } else {
7✔
170
          // Not in on-start mode, just restart normally
171
          await processService.restart(server.pm2Name);
7✔
172
        }
6✔
173

174
        const status = await processService.getStatus(server.pm2Name);
13✔
175
        results.push({
13✔
176
          name: server.name,
13✔
177
          success: true,
13✔
178
          status,
13✔
179
          configRefreshed,
13✔
180
        });
13✔
181
      } catch (error) {
14✔
182
        const message = error instanceof Error ? error.message : String(error);
1!
183
        results.push({
1✔
184
          name: server.name,
1✔
185
          success: false,
1✔
186
          message,
1✔
187
        });
1✔
188
      }
1✔
189
    }
14✔
190

191
    // Return single result if single server was requested
192
    if (options.name && results.length === 1) {
20✔
193
      return results[0];
9✔
194
    }
9✔
195

196
    return results;
4✔
197
  } finally {
20✔
198
    processService.disconnect();
20✔
199
  }
20✔
200
}
20✔
201

202
/**
203
 * CLI action handler for restart command
204
 */
205
export async function restartAction(
6✔
206
  name: string | undefined,
6✔
207
  options: {
6✔
208
    all?: boolean;
209
    tag?: string;
210
    json?: boolean;
211
  },
212
): Promise<void> {
6✔
213
  try {
6✔
214
    if (!name && !options.all && !options.tag) {
6✔
215
      if (options.json) {
2✔
216
        console.log(formatErrorAsJson(new Error("Either server name, --all, or --tag must be specified")));
1✔
217
      } else {
1✔
218
        console.error(formatError("Either server name, --all, or --tag must be specified"));
1✔
219
      }
1✔
220
      process.exitCode = 1;
2✔
221
      return;
2✔
222
    }
2✔
223

224
    const result = await executeRestart({
4✔
225
      name,
4✔
226
      all: options.all,
4✔
227
      tag: options.tag,
4✔
228
    });
4✔
229

230
    const results = Array.isArray(result) ? result : [result];
6!
231

232
    if (options.json) {
6✔
233
      console.log(formatAsJson({ results }));
1✔
234
    } else {
1✔
235
      console.log(formatRestartResult(results));
1✔
236
    }
1✔
237
  } catch (error) {
6✔
238
    if (options.json) {
2✔
239
      console.log(formatErrorAsJson(error));
1✔
240
    } else {
1✔
241
      const message = error instanceof Error ? error.message : String(error);
1!
242
      console.error(formatError(message));
1✔
243
    }
1✔
244
    logger.error({ error }, "Restart command failed");
2✔
245
    process.exitCode = 1;
2✔
246
  }
2✔
247
}
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc