• 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

76.24
/src/cli/commands/refresh.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 { PortService } from "../../services/port.service.js";
5
import type { ServerEntry, ServerStatus } from "../../types/registry.js";
6
import type { GlobalConfig } from "../../types/config.js";
7
import { renderTemplate, renderEnvTemplates, getTemplateVariables, type TemplateContext } from "../../utils/template.js";
8
import {
9
  extractUsedConfigKeys,
10
  createConfigSnapshot,
11
  findServersWithDrift,
12
  formatDrift,
13
} from "../../utils/config-drift.js";
14

15
export interface RefreshCommandOptions {
16
  name?: string;
17
  all?: boolean;
18
  tag?: string;
19
  dryRun?: boolean;
20
}
21

22
export interface RefreshResult {
23
  name: string;
24
  success: boolean;
25
  status?: ServerStatus;
26
  message?: string;
27
  driftDetails?: string;
28
  skipped?: boolean;
29
  /** Whether port was reassigned due to being out of range */
30
  portReassigned?: boolean;
31
  /** Original port before reassignment */
32
  originalPort?: number;
33
  /** New port after reassignment */
34
  newPort?: number;
35
}
36

37
/**
38
 * Build template context for server lookups
39
 */
40
function buildTemplateContext(
6✔
41
  registryService: RegistryService,
6✔
42
  cwd: string,
6✔
43
): TemplateContext {
6✔
44
  return {
6✔
45
    cwd,
6✔
46
    lookupServer: (name: string, lookupCwd?: string) => {
6✔
47
      return registryService.findByCwdAndName(lookupCwd ?? cwd, name);
×
48
    },
×
49
  };
6✔
50
}
6✔
51

52
/**
53
 * Re-resolve a server's command template with current config values
54
 * Updates the registry with new resolved command and config snapshot
55
 * @param newPort - Optional new port if port was reassigned
56
 */
57
async function refreshServerConfig(
5✔
58
  server: ServerEntry,
5✔
59
  config: GlobalConfig,
5✔
60
  registryService: RegistryService,
5✔
61
  templateContext: TemplateContext,
5✔
62
  newPort?: number,
5✔
63
): Promise<{ resolvedCommand: string; port: number }> {
5✔
64
  const port = newPort ?? server.port;
5✔
65

66
  // Get template variables with current config
67
  const templateVars = getTemplateVariables(config, port);
5✔
68

69
  // Re-resolve the command template
70
  const resolvedCommand = renderTemplate(server.command, templateVars, templateContext);
5✔
71

72
  // Re-resolve environment variables if any
73
  const resolvedEnv = server.env
5✔
74
    ? renderEnvTemplates(server.env, templateVars, templateContext)
5!
75
    : {};
×
76

77
  // Extract new used config keys and create new snapshot
78
  const usedConfigKeys = extractUsedConfigKeys(server.command);
5✔
79
  const configSnapshot = createConfigSnapshot(config, usedConfigKeys, server.command);
5✔
80

81
  // Update the registry (include port if it changed)
82
  const updates: Record<string, unknown> = {
5✔
83
    resolvedCommand,
5✔
84
    env: resolvedEnv,
5✔
85
    usedConfigKeys,
5✔
86
    configSnapshot,
5✔
87
  };
5✔
88
  if (newPort !== undefined) {
5!
89
    updates.port = newPort;
×
90
  }
×
91

92
  await registryService.updateServer(server.id, updates);
5✔
93

94
  return { resolvedCommand, port };
5✔
95
}
5✔
96

97
/**
98
 * Execute the refresh command
99
 * Finds servers with config drift and restarts them with updated config
100
 */
101
export async function executeRefresh(options: RefreshCommandOptions): Promise<RefreshResult[]> {
7✔
102
  const registryService = new RegistryService();
7✔
103
  const processService = new ProcessService();
7✔
104
  const configService = new ConfigService();
7✔
105

106
  try {
7✔
107
    // Load registry and config
108
    await registryService.load();
7✔
109
    const config = await configService.load();
7✔
110

111
    // Connect to PM2
112
    await processService.connect();
7✔
113

114
    // Determine which servers to check
115
    let servers: ServerEntry[] = [];
7✔
116

117
    if (options.all) {
7✔
118
      servers = registryService.listServers();
4✔
119
    } else if (options.tag) {
7✔
120
      servers = registryService.listServers({ tag: options.tag });
1✔
121
    } else if (options.name) {
3✔
122
      const server = registryService.findByName(options.name);
2✔
123
      if (!server) {
2✔
124
        throw new Error(`Server "${options.name}" not found`);
1✔
125
      }
1✔
126
      servers = [server];
1✔
127
    } else {
2!
128
      // Default: find all servers with drift
129
      servers = registryService.listServers();
×
130
    }
✔
131

132
    // Find servers with drift
133
    const serversWithDrift = findServersWithDrift(servers, config);
6✔
134

135
    // If no drift, return early
136
    if (serversWithDrift.length === 0) {
7✔
137
      return [{
1✔
138
        name: "",
1✔
139
        success: true,
1✔
140
        skipped: true,
1✔
141
        message: "No servers have config drift",
1✔
142
      }];
1✔
143
    }
1✔
144

145
    const results: RefreshResult[] = [];
5✔
146
    const portService = new PortService(config);
5✔
147

148
    for (const { server, drift } of serversWithDrift) {
7✔
149
      const driftDetails = formatDrift(drift);
6✔
150

151
      // Build template context for this server's cwd
152
      const templateContext = buildTemplateContext(registryService, server.cwd);
6✔
153

154
      // Check if port needs reassignment
155
      let newPort: number | undefined;
6✔
156
      let portReassigned = false;
6✔
157
      if (drift.portOutOfRange) {
6!
158
        const { port } = await portService.assignPort(
×
159
          server.cwd,
×
160
          server.command,
×
161
          undefined, // Use deterministic logic
×
162
        );
×
163
        newPort = port;
×
164
        portReassigned = true;
×
165
      }
×
166

167
      // Dry run mode - just report what would happen
168
      if (options.dryRun) {
6✔
169
        const dryRunResult: RefreshResult = {
1✔
170
          name: server.name,
1✔
171
          success: true,
1✔
172
          skipped: true,
1✔
173
          message: "Would refresh (dry-run mode)",
1✔
174
          driftDetails,
1✔
175
        };
1✔
176
        if (portReassigned) {
1!
177
          dryRunResult.portReassigned = true;
×
178
          dryRunResult.originalPort = server.port;
×
179
          dryRunResult.newPort = newPort;
×
180
          dryRunResult.message = `Would refresh and reassign port ${server.port} → ${newPort} (dry-run mode)`;
×
181
        }
×
182
        results.push(dryRunResult);
1✔
183
        continue;
1✔
184
      }
1✔
185

186
      try {
5✔
187
        // Re-resolve command with new config values (and new port if reassigned)
188
        const { resolvedCommand, port } = await refreshServerConfig(server, config, registryService, templateContext, newPort);
5✔
189

190
        // Delete old process and start with new command
191
        try {
5✔
192
          await processService.delete(server.pm2Name);
5✔
193
        } catch {
6!
194
          // Process might not exist
195
        }
✔
196

197
        // Parse the resolved command to extract script and args
198
        const parts = resolvedCommand.trim().split(/\s+/);
5✔
199
        const script = parts[0] || "node";
6!
200
        const args = parts.slice(1);
6✔
201

202
        // Get the updated server entry for current env
203
        const updatedServer = registryService.findById(server.id);
6✔
204
        const env = updatedServer?.env ?? server.env;
6!
205

206
        await processService.start({
6✔
207
          name: server.pm2Name,
6✔
208
          script,
6✔
209
          args,
6✔
210
          cwd: server.cwd,
6✔
211
          env: {
6✔
212
            ...env,
6✔
213
            PORT: String(port),
6✔
214
          },
6✔
215
        });
6✔
216

217
        const status = await processService.getStatus(server.pm2Name);
5✔
218
        const result: RefreshResult = {
5✔
219
          name: server.name,
5✔
220
          success: true,
5✔
221
          status,
5✔
222
          driftDetails,
5✔
223
        };
5✔
224
        if (portReassigned) {
6!
225
          result.portReassigned = true;
×
226
          result.originalPort = server.port;
×
227
          result.newPort = newPort;
×
228
        }
✔
229
        results.push(result);
5✔
230
      } catch (error) {
6!
231
        const message = error instanceof Error ? error.message : String(error);
×
232
        results.push({
×
233
          name: server.name,
×
234
          success: false,
×
235
          message,
×
236
          driftDetails,
×
237
        });
×
238
      }
×
239
    }
6✔
240

241
    return results;
5✔
242
  } finally {
7✔
243
    processService.disconnect();
7✔
244
  }
7✔
245
}
7✔
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