• 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

81.4
/src/cli/output/formatters.ts
1
import chalk from "chalk";
1✔
2
import Table from "cli-table3";
3
import boxen from "boxen";
4
import type { ServerEntry, ServerStatus } from "../../types/registry.js";
5
import type { InfoCommandResult } from "../commands/info.js";
6
import type { LogsCommandResult } from "../commands/logs.js";
7
import { formatUptime as formatUptimeShared, formatBytes as formatBytesShared } from "../../utils/format.js";
8

9
/**
10
 * Format server status with color
11
 */
12
export function formatStatus(status: ServerStatus): string {
1✔
13
  switch (status) {
28✔
14
    case "online":
28✔
15
      return chalk.green("● online");
23✔
16
    case "stopped":
28✔
17
      return chalk.gray("○ stopped");
2✔
18
    case "errored":
28✔
19
      return chalk.red("✖ errored");
1✔
20
    default:
28✔
21
      return chalk.yellow("? unknown");
2✔
22
  }
28✔
23
}
28✔
24

25
/**
26
 * Format URL for display
27
 */
28
export function formatUrl(protocol: string, hostname: string, port: number): string {
1✔
29
  return chalk.cyan(`${protocol}://${hostname}:${port}`);
2✔
30
}
2✔
31

32
/**
33
 * Format server name for display
34
 */
35
export function formatName(name: string): string {
1✔
36
  return chalk.bold(name);
8✔
37
}
8✔
38

39
/**
40
 * Create a table showing server list
41
 */
42
export interface ServerListItem {
43
  server: ServerEntry;
44
  status: ServerStatus;
45
  hasDrift?: boolean;
46
}
47

48
export function formatServerListTable(servers: ServerListItem[]): string {
1✔
49
  if (servers.length === 0) {
6✔
50
    return chalk.yellow("No servers registered");
1✔
51
  }
1✔
52

53
  const table = new Table({
5✔
54
    head: [
5✔
55
      chalk.bold("Name"),
5✔
56
      chalk.bold("Status"),
5✔
57
      chalk.bold("Port"),
5✔
58
      chalk.bold("Command"),
5✔
59
      chalk.bold("Working Directory"),
5✔
60
    ],
5✔
61
    style: {
5✔
62
      head: [],
5✔
63
      border: [],
5✔
64
    },
5✔
65
  });
5✔
66

67
  for (const { server, status, hasDrift } of servers) {
6✔
68
    // Add drift indicator to name if config has drifted
69
    const nameDisplay = hasDrift
7!
70
      ? formatName(server.name) + chalk.yellow(" ⚡")
×
71
      : formatName(server.name);
7✔
72

73
    table.push([
7✔
74
      nameDisplay,
7✔
75
      formatStatus(status),
7✔
76
      String(server.port),
7✔
77
      truncateString(server.command, 30),
7✔
78
      truncatePath(server.cwd, 30),
7✔
79
    ]);
7✔
80
  }
7✔
81

82
  // Add legend if any server has drift
83
  const anyDrift = servers.some(s => s.hasDrift);
5✔
84
  let result = table.toString();
5✔
85
  if (anyDrift) {
6!
86
    result += "\n\n" + chalk.yellow("⚡ = Config has changed since server started. Run `servherd refresh` to update.");
×
87
  }
✔
88

89
  return result;
5✔
90
}
5✔
91

92
/**
93
 * Format start result output
94
 */
95
export interface StartResult {
96
  action: "started" | "existing" | "restarted" | "renamed" | "refreshed";
97
  server: ServerEntry;
98
  status: ServerStatus;
99
  previousName?: string;
100
  /** Whether port was reassigned due to unavailability or config change */
101
  portReassigned?: boolean;
102
  /** Original port before reassignment */
103
  originalPort?: number;
104
  /** Whether config drift was detected and applied */
105
  configDrift?: boolean;
106
  /** Details of config drift that was applied */
107
  driftDetails?: string[];
108
  /** User declined refresh when prompted */
109
  userDeclinedRefresh?: boolean;
110
}
111

112
export function formatStartResult(result: StartResult): string {
1✔
113
  const { action, server, status } = result;
3✔
114
  const url = `${server.protocol}://${server.hostname}:${server.port}`;
3✔
115

116
  const lines: string[] = [];
3✔
117

118
  switch (action) {
3✔
119
    case "started":
3✔
120
      lines.push(chalk.green(`✓ Server "${server.name}" started`));
1✔
121
      break;
1✔
122
    case "existing":
3✔
123
      lines.push(chalk.blue(`ℹ Server "${server.name}" already exists`));
1✔
124
      break;
1✔
125
    case "restarted":
3✔
126
      lines.push(chalk.green(`✓ Server "${server.name}" restarted`));
1✔
127
      break;
1✔
128
    case "renamed":
3!
129
      lines.push(chalk.green(`✓ Server renamed from "${result.previousName}" to "${server.name}"`));
×
130
      break;
×
131
    case "refreshed":
3!
132
      lines.push(chalk.green(`↻ Server "${server.name}" refreshed (config changed)`));
×
133
      break;
×
134
  }
3✔
135

136
  lines.push(`  ${chalk.bold("Name:")}   ${server.name}`);
3✔
137
  lines.push(`  ${chalk.bold("Port:")}   ${server.port}`);
3✔
138
  lines.push(`  ${chalk.bold("URL:")}    ${chalk.cyan(url)}`);
3✔
139
  lines.push(`  ${chalk.bold("Status:")} ${formatStatus(status)}`);
3✔
140
  lines.push(`  ${chalk.bold("CWD:")}    ${server.cwd}`);
3✔
141

142
  // Show port reassignment if it happened
143
  if (result.portReassigned && result.originalPort !== undefined) {
3!
144
    lines.push(`  ${chalk.yellow(`⚠ Port reassigned: ${result.originalPort} → ${server.port}`)}`);
×
145
  }
×
146

147
  // Show config drift details if present
148
  if (result.driftDetails && result.driftDetails.length > 0) {
3!
149
    lines.push(`  ${chalk.bold("Config changes applied:")}`);
×
150
    for (const detail of result.driftDetails) {
×
151
      lines.push(`    ${chalk.dim("•")} ${detail}`);
×
152
    }
×
153
  }
×
154

155
  // Show user declined message if applicable
156
  if (result.userDeclinedRefresh) {
3!
157
    lines.push(`  ${chalk.yellow("⚠ Config has changed but refresh was declined")}`);
×
158
  }
×
159

160
  return lines.join("\n");
3✔
161
}
3✔
162

163
/**
164
 * Format stop result output
165
 */
166
export interface StopResult {
167
  name: string;
168
  success: boolean;
169
  message?: string;
170
}
171

172
export function formatStopResult(results: StopResult[]): string {
1✔
173
  if (results.length === 0) {
6✔
174
    return chalk.yellow("No servers to stop");
1✔
175
  }
1✔
176

177
  const lines: string[] = [];
5✔
178

179
  for (const result of results) {
6✔
180
    if (result.success) {
6✔
181
      lines.push(chalk.green(`✓ Server "${result.name}" stopped`));
3✔
182
    } else {
3✔
183
      lines.push(chalk.red(`✖ Failed to stop "${result.name}": ${result.message}`));
3✔
184
    }
3✔
185
  }
6✔
186

187
  return lines.join("\n");
5✔
188
}
5✔
189

190
/**
191
 * Format error message
192
 */
193
export function formatError(message: string): string {
1✔
194
  return chalk.red(`✖ Error: ${message}`);
7✔
195
}
7✔
196

197
/**
198
 * Format success message
199
 */
200
export function formatSuccess(message: string): string {
1✔
201
  return chalk.green(`✓ ${message}`);
3✔
202
}
3✔
203

204
/**
205
 * Format info message
206
 */
207
export function formatInfo(message: string): string {
1✔
208
  return chalk.blue(`ℹ ${message}`);
1✔
209
}
1✔
210

211
/**
212
 * Format warning message
213
 */
214
export function formatWarning(message: string): string {
1✔
215
  return chalk.yellow(`⚠ ${message}`);
2✔
216
}
2✔
217

218
/**
219
 * Truncate a string for display
220
 */
221
function truncateString(str: string, maxLength: number): string {
7✔
222
  if (str.length <= maxLength) {
7✔
223
    return str;
6✔
224
  }
6✔
225
  return str.slice(0, maxLength - 3) + "...";
1✔
226
}
1✔
227

228
/**
229
 * Truncate a path for display
230
 */
231
function truncatePath(path: string, maxLength: number): string {
7✔
232
  if (path.length <= maxLength) {
7✔
233
    return path;
6✔
234
  }
6✔
235

236
  const parts = path.split("/");
1✔
237
  let result = "";
1✔
238

239
  // Start from the end and work backward
240
  for (let i = parts.length - 1; i >= 0; i--) {
1✔
241
    const candidate = parts.slice(i).join("/");
1✔
242
    if (candidate.length <= maxLength - 3) {
1✔
243
      result = "..." + (i > 0 ? "/" : "") + candidate;
1!
244
      break;
1✔
245
    }
1✔
246
  }
1✔
247

248
  if (!result) {
7!
249
    // If even the last part is too long, just truncate
250
    result = "..." + path.slice(-(maxLength - 3));
×
251
  }
✔
252

253
  return result;
1✔
254
}
1✔
255

256
/**
257
 * Format bytes to human readable string
258
 * Uses shared utility from utils/format.ts
259
 */
260
function formatBytes(bytes: number): string {
4✔
261
  return formatBytesShared(bytes);
4✔
262
}
4✔
263

264
/**
265
 * Format uptime to human readable string
266
 * Takes a start timestamp and formats the duration since that time
267
 * Uses shared utility from utils/format.ts
268
 */
269
function formatUptime(startTimestamp: number): string {
5✔
270
  const durationMs = Date.now() - startTimestamp;
5✔
271
  return formatUptimeShared(durationMs);
5✔
272
}
5✔
273

274
/**
275
 * Format server info as a boxed display
276
 */
277
export function formatServerInfo(info: InfoCommandResult): string {
1✔
278
  const lines: string[] = [];
9✔
279

280
  // Header
281
  lines.push(chalk.bold.cyan(`Server: ${info.name}`));
9✔
282
  lines.push("");
9✔
283

284
  // Status and basic info
285
  lines.push(`${chalk.bold("Status:")}      ${formatStatus(info.status)}`);
9✔
286
  lines.push(`${chalk.bold("URL:")}         ${chalk.cyan(info.url)}`);
9✔
287
  lines.push(`${chalk.bold("Port:")}        ${info.port}`);
9✔
288
  lines.push(`${chalk.bold("Hostname:")}    ${info.hostname}`);
9✔
289
  lines.push(`${chalk.bold("Protocol:")}    ${info.protocol}`);
9✔
290
  lines.push("");
9✔
291

292
  // Process info
293
  if (info.pid) {
9✔
294
    lines.push(`${chalk.bold("PID:")}         ${info.pid}`);
1✔
295
  }
1✔
296
  if (info.uptime) {
9✔
297
    lines.push(`${chalk.bold("Uptime:")}      ${formatUptime(info.uptime)}`);
5✔
298
  }
5✔
299
  if (info.restarts !== undefined) {
9✔
300
    lines.push(`${chalk.bold("Restarts:")}    ${info.restarts}`);
1✔
301
  }
1✔
302
  if (info.memory !== undefined) {
9✔
303
    lines.push(`${chalk.bold("Memory:")}      ${formatBytes(info.memory)}`);
4✔
304
  }
4✔
305
  if (info.cpu !== undefined) {
9✔
306
    lines.push(`${chalk.bold("CPU:")}         ${info.cpu.toFixed(1)}%`);
1✔
307
  }
1✔
308
  lines.push("");
9✔
309

310
  // Command info
311
  lines.push(`${chalk.bold("Command:")}     ${info.command}`);
9✔
312
  lines.push(`${chalk.bold("Resolved:")}    ${info.resolvedCommand}`);
9✔
313
  lines.push(`${chalk.bold("CWD:")}         ${info.cwd}`);
9✔
314
  lines.push(`${chalk.bold("PM2 Name:")}    ${info.pm2Name}`);
9✔
315
  lines.push("");
9✔
316

317
  // Optional fields
318
  if (info.description) {
9✔
319
    lines.push(`${chalk.bold("Description:")} ${info.description}`);
1✔
320
  }
1✔
321
  if (info.tags && info.tags.length > 0) {
9✔
322
    lines.push(`${chalk.bold("Tags:")}        ${info.tags.join(", ")}`);
1✔
323
  }
1✔
324

325
  // Log paths
326
  if (info.outLogPath) {
9✔
327
    lines.push(`${chalk.bold("Out Log:")}     ${info.outLogPath}`);
1✔
328
  }
1✔
329
  if (info.errLogPath) {
9✔
330
    lines.push(`${chalk.bold("Err Log:")}     ${info.errLogPath}`);
1✔
331
  }
1✔
332

333
  // Environment variables
334
  if (info.env && Object.keys(info.env).length > 0) {
9✔
335
    lines.push("");
1✔
336
    lines.push(chalk.bold("Environment:"));
1✔
337
    for (const [key, value] of Object.entries(info.env)) {
1✔
338
      lines.push(`  ${key}=${value}`);
1✔
339
    }
1✔
340
  }
1✔
341

342
  // Created at
343
  lines.push("");
9✔
344
  lines.push(`${chalk.bold("Created:")}     ${new Date(info.createdAt).toLocaleString()}`);
9✔
345

346
  return boxen(lines.join("\n"), {
9✔
347
    padding: 1,
9✔
348
    margin: 0,
9✔
349
    borderStyle: "round",
9✔
350
    borderColor: "cyan",
9✔
351
  });
9✔
352
}
9✔
353

354
/**
355
 * Format logs output
356
 */
357
export function formatLogs(result: LogsCommandResult): string {
1✔
358
  const lines: string[] = [];
2✔
359

360
  lines.push(chalk.bold.cyan(`Logs for: ${result.name}`));
2✔
361
  lines.push(`${chalk.bold("Status:")} ${formatStatus(result.status)}`);
2✔
362

363
  if (result.outLogPath) {
2✔
364
    lines.push(`${chalk.bold("Log file:")} ${result.outLogPath}`);
1✔
365
  }
1✔
366

367
  lines.push(`${chalk.bold("Lines:")} ${result.lines}`);
2✔
368
  lines.push("");
2✔
369
  lines.push(chalk.gray("─".repeat(60)));
2✔
370
  lines.push("");
2✔
371

372
  if (result.logs) {
2✔
373
    lines.push(result.logs);
1✔
374
  } else {
1✔
375
    lines.push(chalk.gray("(no logs available)"));
1✔
376
  }
1✔
377

378
  return lines.join("\n");
2✔
379
}
2✔
380

381
/**
382
 * Format restart result output
383
 */
384
export interface RestartResult {
385
  name: string;
386
  success: boolean;
387
  status?: ServerStatus;
388
  message?: string;
389
  configRefreshed?: boolean;
390
}
391

392
export function formatRestartResult(results: RestartResult[]): string {
1✔
393
  if (results.length === 0) {
5✔
394
    return chalk.yellow("No servers to restart");
1✔
395
  }
1✔
396

397
  const lines: string[] = [];
4✔
398

399
  for (const result of results) {
5✔
400
    if (result.success) {
5✔
401
      if (result.configRefreshed) {
3!
402
        lines.push(chalk.green(`✓ Server "${result.name}" restarted with updated config`));
×
403
      } else {
3✔
404
        lines.push(chalk.green(`✓ Server "${result.name}" restarted`));
3✔
405
      }
3✔
406
      if (result.status) {
3✔
407
        lines.push(`  Status: ${formatStatus(result.status)}`);
3✔
408
      }
3✔
409
    } else {
3✔
410
      lines.push(chalk.red(`✖ Failed to restart "${result.name}": ${result.message}`));
2✔
411
    }
2✔
412
  }
5✔
413

414
  return lines.join("\n");
4✔
415
}
4✔
416

417
/**
418
 * Format refresh result output
419
 */
420
export interface RefreshResult {
421
  name: string;
422
  success: boolean;
423
  status?: ServerStatus;
424
  message?: string;
425
  driftDetails?: string;
426
  skipped?: boolean;
427
}
428

429
function formatRefreshResult(results: RefreshResult[], dryRun?: boolean): string {
×
430
  // Check if this is a "no drift" result
431
  if (results.length === 1 && results[0].skipped && results[0].name === "") {
×
432
    return chalk.blue(`ℹ ${results[0].message}`);
×
433
  }
×
434

435
  const lines: string[] = [];
×
436

437
  if (dryRun) {
×
438
    lines.push(chalk.yellow("Dry run mode - no changes made"));
×
439
    lines.push("");
×
440
  }
×
441

442
  for (const result of results) {
×
443
    if (result.skipped && dryRun) {
×
444
      lines.push(chalk.yellow(`⚠ Server "${result.name}" would be refreshed`));
×
445
      if (result.driftDetails) {
×
446
        lines.push(chalk.gray(`  ${result.driftDetails.split("\n").join("\n  ")}`));
×
447
      }
×
448
    } else if (result.success) {
×
449
      lines.push(chalk.green(`✓ Server "${result.name}" refreshed with updated config`));
×
450
      if (result.status) {
×
451
        lines.push(`  Status: ${formatStatus(result.status)}`);
×
452
      }
×
453
    } else {
×
454
      lines.push(chalk.red(`✖ Failed to refresh "${result.name}": ${result.message}`));
×
455
    }
×
456
  }
×
457

458
  return lines.join("\n");
×
459
}
×
460

461
/**
462
 * Format remove result output
463
 */
464
export interface RemoveResult {
465
  name: string;
466
  success: boolean;
467
  cancelled?: boolean;
468
  message?: string;
469
}
470

471
export function formatRemoveResult(results: RemoveResult[]): string {
1✔
472
  if (results.length === 0) {
5✔
473
    return chalk.yellow("No servers to remove");
1✔
474
  }
1✔
475

476
  const lines: string[] = [];
4✔
477

478
  for (const result of results) {
5✔
479
    if (result.success) {
6✔
480
      lines.push(chalk.green(`✓ Server "${result.name}" removed`));
2✔
481
    } else if (result.cancelled) {
6✔
482
      lines.push(chalk.yellow(`⚠ Removal of "${result.name}" cancelled`));
2✔
483
    } else {
2✔
484
      lines.push(chalk.red(`✖ Failed to remove "${result.name}": ${result.message}`));
2✔
485
    }
2✔
486
  }
6✔
487

488
  return lines.join("\n");
4✔
489
}
4✔
490

491
/**
492
 * Format config result output
493
 */
494
export interface ConfigResult {
495
  // For --show
496
  config?: Record<string, unknown>;
497
  configPath?: string | null;
498
  globalConfigPath?: string;
499

500
  // For --get
501
  key?: string;
502
  value?: unknown;
503

504
  // For --set
505
  updated?: boolean;
506
  refreshMessage?: string;
507

508
  // For --reset
509
  reset?: boolean;
510
  cancelled?: boolean;
511

512
  // For --refresh
513
  refreshResults?: RefreshResult[];
514
  dryRun?: boolean;
515

516
  // For --list-vars
517
  variables?: Record<string, string>;
518

519
  // For --add
520
  addedVar?: boolean;
521
  varName?: string;
522
  varValue?: string;
523

524
  // For --remove
525
  removedVar?: boolean;
526

527
  // For errors
528
  error?: string;
529
}
530

531
export function formatConfigResult(result: ConfigResult): string {
1✔
532
  const lines: string[] = [];
14✔
533

534
  // Handle error
535
  if (result.error) {
14✔
536
    return formatError(result.error);
2✔
537
  }
2✔
538

539
  // Handle --refresh / --refresh-all
540
  if (result.refreshResults) {
14!
541
    return formatRefreshResult(result.refreshResults, result.dryRun);
×
542
  }
✔
543

544
  // Handle --list-vars
545
  if (result.variables !== undefined) {
14!
546
    const vars = result.variables;
×
547
    const varKeys = Object.keys(vars);
×
548

549
    if (varKeys.length === 0) {
×
550
      return formatInfo("No custom variables defined. Use 'servherd config --add <name> --value <value>' to add one.");
×
551
    }
×
552

553
    lines.push(chalk.bold.cyan("Custom Template Variables"));
×
554
    lines.push("");
×
555

556
    const table = new Table({
×
557
      head: [chalk.bold("Variable"), chalk.bold("Value")],
×
558
      style: { head: [], border: [] },
×
559
    });
×
560

561
    for (const [name, value] of Object.entries(vars)) {
×
562
      table.push([`{{${name}}}`, value]);
×
563
    }
×
564

565
    lines.push(table.toString());
×
566
    return lines.join("\n");
×
567
  }
✔
568

569
  // Handle --add
570
  if (result.addedVar !== undefined) {
14!
571
    if (result.addedVar) {
×
572
      return formatSuccess(`Variable "{{${result.varName}}}" set to "${result.varValue}"`);
×
573
    }
×
574
    return formatError(result.error || "Failed to add variable");
×
575
  }
✔
576

577
  // Handle --remove
578
  if (result.removedVar !== undefined) {
14!
579
    if (result.removedVar) {
×
580
      return formatSuccess(`Variable "{{${result.varName}}}" removed`);
×
581
    }
×
582
    return formatError(result.error || "Failed to remove variable");
×
583
  }
✔
584

585
  // Handle --get
586
  if (result.key !== undefined && result.value !== undefined && result.updated === undefined && result.reset === undefined) {
14✔
587
    return `${chalk.bold(result.key)}: ${formatValue(result.value)}`;
3✔
588
  }
3✔
589

590
  // Handle --set
591
  if (result.updated !== undefined) {
14✔
592
    if (result.updated) {
2✔
593
      let message = formatSuccess(`Configuration "${result.key}" set to ${formatValue(result.value)}`);
1✔
594
      if (result.refreshMessage) {
1!
595
        message += "\n" + chalk.blue(`ℹ ${result.refreshMessage}`);
×
596
      }
×
597
      return message;
1✔
598
    }
1✔
599
    return formatError(result.error || "Failed to update configuration");
2✔
600
  }
2✔
601

602
  // Handle --reset
603
  if (result.reset !== undefined) {
14✔
604
    if (result.reset) {
3✔
605
      return formatSuccess("Configuration reset to defaults");
1✔
606
    }
1✔
607
    if (result.cancelled) {
3✔
608
      return formatWarning("Reset cancelled");
1✔
609
    }
1✔
610
    return formatError("Failed to reset configuration");
1✔
611
  }
1✔
612

613
  // Handle --show (default)
614
  if (result.config) {
14✔
615
    lines.push(chalk.bold.cyan("Current Configuration"));
3✔
616
    lines.push("");
3✔
617

618
    if (result.configPath) {
3✔
619
      lines.push(`${chalk.bold("Loaded from:")} ${result.configPath}`);
1✔
620
    } else if (result.globalConfigPath) {
3✔
621
      lines.push(`${chalk.bold("Global config:")} ${result.globalConfigPath}`);
2✔
622
    }
2✔
623
    lines.push("");
3✔
624

625
    const table = new Table({
3✔
626
      head: [chalk.bold("Setting"), chalk.bold("Value")],
3✔
627
      style: { head: [], border: [] },
3✔
628
    });
3✔
629

630
    // Flatten config for display
631
    const flatConfig = flattenConfig(result.config);
3✔
632
    for (const [key, value] of Object.entries(flatConfig)) {
3✔
633
      table.push([key, formatValue(value)]);
7✔
634
    }
7✔
635

636
    lines.push(table.toString());
3✔
637

638
    return lines.join("\n");
3✔
639
  }
3✔
640

641
  return "";
1✔
642
}
1✔
643

644
/**
645
 * Format a value for display
646
 */
647
function formatValue(value: unknown): string {
11✔
648
  if (value === null || value === undefined) {
11✔
649
    return chalk.gray("(not set)");
1✔
650
  }
1✔
651
  if (typeof value === "object") {
11!
652
    return JSON.stringify(value);
×
653
  }
✔
654
  return String(value);
10✔
655
}
10✔
656

657
/**
658
 * Flatten a nested config object for display
659
 */
660
function flattenConfig(obj: Record<string, unknown>, prefix = ""): Record<string, unknown> {
5✔
661
  const result: Record<string, unknown> = {};
5✔
662

663
  for (const [key, value] of Object.entries(obj)) {
5✔
664
    const fullKey = prefix ? `${prefix}.${key}` : key;
9✔
665

666
    if (value !== null && typeof value === "object" && !Array.isArray(value)) {
9✔
667
      Object.assign(result, flattenConfig(value as Record<string, unknown>, fullKey));
2✔
668
    } else {
9✔
669
      result[fullKey] = value;
7✔
670
    }
7✔
671
  }
9✔
672

673
  return result;
5✔
674
}
5✔
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